Compare commits

...

613 Commits

Author SHA1 Message Date
muzam1l
0fce9af77c Fetch sequence on account creation. 2023-03-10 16:18:35 +05:30
muzam1l
55c68c580a Account Sequence UI. 2023-03-10 16:02:32 +05:30
muzamil
832a7997d1 Merge pull request #288 from XRPLF/fix/tx
Fix tx json saving and discarding.
2023-03-08 21:32:36 +05:30
muzam1l
4528e5a16e Actually fix Json 'save'. 2023-03-08 20:33:23 +05:30
muzam1l
38f064c6d8 Fix json saving and discarding. 2023-03-08 17:13:36 +05:30
muzamil
fbf4565dbc Merge pull request #287 from XRPLF/feat/memos-ui
Memos UI
2023-03-07 17:32:32 +05:30
muzam1l
9001c64fed minor label changes. 2023-03-07 16:58:32 +05:30
muzam1l
03b768db4e UI for memos fields. 2023-03-07 16:03:47 +05:30
muzam1l
825af0db89 Add memos field in transactions. 2023-03-06 21:14:14 +05:30
muzamil
31043f33ab Merge pull request #286 from XRPLF/feat/tx-params-ui
HookParameters UI for transactions.
2023-03-06 16:06:19 +05:30
muzam1l
39699a1cb9 HookParameters UI for transactions. 2023-03-03 18:32:03 +05:30
muzam1l
b50b300307 Refactor tx. 2023-03-03 16:00:43 +05:30
muzamil
82c06cbb12 Merge pull request #285 from XRPLF/fix/params
Add invoke hookon option.
2023-03-02 19:17:38 +05:30
muzam1l
423ee18e6a Add Invoke HookOn option. 2023-03-02 15:11:44 +05:30
muzamil
3bb26d0c9b Merge pull request #281 from XRPLF/feat/testnet-v3
Testnet v3.
2023-02-10 16:12:22 +05:30
muzam1l
43c83d0de6 Fix delete hook and some refactor. 2023-02-10 14:43:51 +05:30
muzam1l
6bb407cb0f Better package loading experience. 2023-02-09 22:15:22 +05:30
muzamil
d7b29ba809 Merge pull request #282 from XRPLF/feat/bundle-xrpl
Bundle patched `xrpl-accountlib` and expose through require syntax.
2023-02-09 20:22:43 +05:30
muzam1l
ca81d8ad41 Bundle xrpl-accountlib and expose throw require syntax. 2023-02-09 15:41:06 +05:30
muzam1l
a7e59d7b73 Add UriToken transaction. 2023-02-07 14:32:06 +05:30
muzam1l
8f6b28cef5 Add Invoke transaction type. 2023-02-07 14:15:07 +05:30
muzam1l
c1815f272b refactor. 2023-02-07 14:00:57 +05:30
muzam1l
76871a8041 Testnet v3. 2023-02-06 21:09:11 +05:30
mariopil
d2033c8035 Hooks docs update (#279)
* Added hooks-guard-call-non-const checker doc, updated hooks-guard-in-for doc.
2023-01-09 13:43:32 +01:00
muzamil
48daf1c5c8 Merge pull request #278 from XRPLF/feat/compile-wat
Increase wat file priority in sorting.
2022-12-07 15:03:40 +05:30
muzam1l
a3365e4beb Increase wat file priority in sorting. 2022-12-07 15:02:09 +05:30
muzamil
45d813cdad Merge pull request #277 from XRPLF/feat/compile-wat
Compile raw wat files.
2022-12-07 14:46:05 +05:30
muzam1l
911416aa2f Compile wat files. 2022-12-07 13:31:59 +05:30
muzamil
2d836af9ed Merge pull request #276 from XRPLF/feat/flags-ui
Transaction flags UI.
2022-11-01 17:14:44 +05:30
muzam1l
4f0fc838be Minor css fix. 2022-11-01 11:20:09 +05:30
muzam1l
fa93912c38 Remove reductant comment. 2022-10-28 14:58:11 +05:30
muzam1l
f5cb76c302 Merge branch 'main' into feat/flags-ui 2022-10-28 14:48:41 +05:30
muzamil
df1d65dcab Merge pull request #275 from XRPLF/dependabot/npm_and_yarn/jose-4.10.0
Bump jose from 4.6.0 to 4.10.0
2022-10-28 14:48:02 +05:30
muzam1l
1513f78991 Add tx specific flags. 2022-10-28 14:43:37 +05:30
muzam1l
3a064f307b Add global flags. 2022-10-28 14:21:22 +05:30
muzam1l
6fca05f310 Add flags UI to Payment transaction. 2022-10-28 12:29:57 +05:30
muzamil
31e67d382f Merge pull request #273 from XRPLF/fix/renaming-ext
Update file language on renaming.
2022-10-26 18:10:43 +05:30
dependabot[bot]
27475301e4 Bump jose from 4.6.0 to 4.10.0
Bumps [jose](https://github.com/panva/jose) from 4.6.0 to 4.10.0.
- [Release notes](https://github.com/panva/jose/releases)
- [Changelog](https://github.com/panva/jose/blob/main/CHANGELOG.md)
- [Commits](https://github.com/panva/jose/compare/v4.6.0...v4.10.0)

---
updated-dependencies:
- dependency-name: jose
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-14 08:29:14 +00:00
muzam1l
934283976a Add plain text language to monaco. 2022-09-09 14:07:13 +05:30
muzamil
2d9ca2674e Merge pull request #266 from XRPLF/feat/engine-code-links
Linkify engine error codes and some refactor.
2022-08-19 20:00:22 +05:30
muzam1l
221c727af6 Fix mistake in set-codes json. 2022-08-19 18:33:31 +05:30
muzam1l
3b0a8c44c9 Remove link title from log. 2022-08-19 17:46:02 +05:30
muzam1l
0d9e9e7b45 Merge branch 'main' into fix/renaming-ext 2022-08-19 15:14:52 +05:30
muzam1l
094f739b80 Merge branch 'main' into feat/engine-code-links 2022-08-19 15:07:44 +05:30
muzamil
a2077f9592 Merge pull request #270 from XRPLF/feat/prettier
Prettier config!
2022-08-17 22:11:44 +05:30
muzam1l
59d9c5356c Run format again on new changes. 2022-08-17 22:07:36 +05:30
muzam1l
2a10d525fb Merge branch 'main' into feat/prettier 2022-08-17 22:05:50 +05:30
muzam1l
2c2bf59bcd Update file language on renaming. 2022-08-17 12:46:17 +05:30
muzamil
79bd5da3d2 Merge pull request #269 from XRPLF/feat/md-preview
Markdown Preview.
2022-08-17 12:39:42 +05:30
muzam1l
1fab69ef9b Minor changes based on feedback. 2022-08-17 12:11:36 +05:30
muzam1l
6418094b0f Run prettier through everything. 2022-08-17 11:50:49 +05:30
muzam1l
2d82966b3b Tooltip for SetHook codes in debug stream. 2022-08-16 18:16:49 +05:30
muzam1l
dbbbfdb2f0 Increase C files priority in sorting. 2022-08-16 13:42:38 +05:30
muzam1l
9923dd9390 Add prettier config. 2022-08-12 23:15:05 +05:30
muzam1l
052be354d1 Fix sorting logic 2022-08-12 23:06:58 +05:30
muzam1l
737523fe8d Log full error on console in fetchFiles. 2022-08-12 16:31:25 +05:30
muzam1l
034fc3423b Link comp in markdown. 2022-08-12 15:54:34 +05:30
muzam1l
b23b0066bb Change carbon and starter templates to forked. 2022-08-12 15:43:00 +05:30
muzam1l
380b6b1afd Fix linting error 2022-08-12 15:40:15 +05:30
muzam1l
c7d77b26b5 Save markdown before previewing. 2022-08-12 15:36:46 +05:30
muzam1l
aa62251780 Fix gist name. 2022-08-12 15:32:53 +05:30
muzam1l
4b2b4b25c0 Give md extension higher priority for sorting! 2022-08-12 15:22:57 +05:30
muzam1l
6dd5712573 Refactor fetchFiles.ts 2022-08-12 15:21:59 +05:30
muzam1l
6611a94652 Add markdown preview and toggle to md files. 2022-08-11 15:12:31 +05:30
muzam1l
93c5ef231e Fix display of nullish values in script logs. 2022-08-09 17:18:22 +05:30
muzam1l
53c2104b94 Point groups of error codes to their individual pages. 2022-08-09 17:00:30 +05:30
muzam1l
c336ff8334 Linkify engine error codes and some refactor. 2022-08-09 02:07:15 +05:30
muzam1l
2086291d4d Revert "Linkify engine error codes and some refactor."
This reverts commit 5505f0ac87.
2022-08-09 01:55:42 +05:30
muzam1l
5505f0ac87 Linkify engine error codes and some refactor. 2022-08-09 01:50:29 +05:30
muzamil
2cf92b908d Merge pull request #263 from XRPLF/fix/delete-account-effects
Update tx state accounts on delete account.
2022-08-08 15:56:28 +05:30
Valtteri Karesto
70de876f75 Merge pull request #265 from XRPLF/fix/update-next-auth
Update next-auth to fix dependabot alert
2022-08-08 13:23:43 +03:00
Valtteri Karesto
58cde29fff Update next-auth to fix dependabot alert 2022-08-08 09:07:45 +03:00
muzamil
a06fb06610 Merge pull request #264 from XRPLF/fix/tab-switching
Switch to correct tab after renaming/closing.
2022-08-05 16:23:38 +05:30
muzam1l
1f5a9731bb Readonly files are not renamable now! 2022-08-04 18:54:20 +05:30
muzam1l
ef4f95ca3e Switch to correct tab after renaming/closing. 2022-08-03 16:02:17 +05:30
muzamil
fb9814ec76 Merge pull request #262 from XRPLF/fix/script-log-appending
Clear script log on running new script.
2022-08-03 14:33:59 +05:30
muzam1l
7f6b989f15 Update tx state accounts on delete account. 2022-08-02 15:23:11 +05:30
muzam1l
d459b2ee92 Clear script log on running new script. 2022-08-01 17:04:26 +05:30
muzamil
6ee1a09aaa Merge pull request #259 from XRPLF/feat/deploy-default-fields
Deploy config default fields from source files.
2022-07-29 15:59:01 +05:30
muzam1l
dd2228fb35 Reset deploy form fields when file changes. 2022-07-29 14:33:28 +05:30
muzam1l
ca52a5e064 Enforce required prop in default tags. 2022-07-28 18:12:58 +05:30
muzam1l
df0f8abe62 Add required margin to param field. 2022-07-27 17:31:43 +05:30
muzam1l
a6c4db1951 Invoke options defaults. 2022-07-26 17:05:33 +05:30
muzam1l
1c91003164 Deploy config default fields from source files. 2022-07-25 20:22:58 +05:30
muzamil
66be0efbbd Merge pull request #255 from XRPLF/feat/tab-renames
Implement file renaming.
2022-07-22 16:35:47 +05:30
muzamil
9ab64ec062 Merge pull request #256 from XRPLF/fix/compile-result
Fix incorrect compilation result.
2022-07-22 14:43:55 +05:30
muzamil
e77a5e234f Merge pull request #257 from XRPLF/fix/account-select
Fixes #240.
2022-07-22 14:43:32 +05:30
muzamil
d2f618512a Merge pull request #258 from XRPLF/fix/acc-import-limit
Remove account import limit.
2022-07-22 14:43:01 +05:30
muzam1l
f5063de2c9 Fix matching file error algo for rename/newfile. 2022-07-22 14:40:40 +05:30
muzam1l
1ee8dcb536 Select comp: remove hightlight from selected option 2022-07-21 16:58:07 +05:30
muzam1l
7f6f9c11db Fix dialog z-index for safari. 2022-07-21 16:44:33 +05:30
muzam1l
b2b7059774 Remove account import limit. 2022-07-21 15:31:06 +05:30
muzam1l
41ba096ef9 Account selector change. 2022-07-21 15:26:37 +05:30
muzam1l
8b72086c04 Differentiate between netwrok error and invalid binary error 2022-07-21 15:18:28 +05:30
muzam1l
895b34cc68 Fix compile result message on invalid wasm 2022-07-21 15:03:16 +05:30
muzam1l
b9da659f83 Fix that weird anonymous zero in the ui. 2022-07-21 14:14:40 +05:30
muzamil
3897f2d823 Merge pull request #246 from XRPLF/feat/tabs-comp
Tabs and Context menu components.
2022-07-20 17:46:04 +05:30
muzam1l
6a3ff3e1d7 Implement tab renaming. 2022-07-20 16:56:55 +05:30
muzam1l
bf792f1495 Fix dependecy issues. 2022-07-20 14:55:59 +05:30
muzam1l
df3210a663 Add context menu component 2022-07-20 14:12:48 +05:30
muzam1l
bad7730c32 Fix extension requirement error. 2022-07-20 14:05:02 +05:30
muzam1l
adb6a78549 update active compiled file based on active editor file. 2022-07-20 14:05:02 +05:30
muzam1l
8cc27f20c3 Migrate File navigation to tabs component. 2022-07-20 14:05:02 +05:30
muzamil
8e49a3f5f1 Merge pull request #253 from XRPLF/fix/json-tx
Fix json mode.
2022-07-20 13:59:42 +05:30
muzam1l
3179757469 Fix TxFields type. 2022-07-19 17:46:13 +05:30
muzam1l
554cfb3db9 Add fee field to all transactions. 2022-07-19 17:45:41 +05:30
muzam1l
637a066f69 Fix tx reset state button 2022-07-19 17:36:09 +05:30
muzam1l
c9a852e9be Add destination field to NFTokenCreateOffer 2022-07-19 16:05:10 +05:30
muzam1l
307a5407eb Fix handling of Destination field in transactions. 2022-07-19 16:01:21 +05:30
muzam1l
faa28845c8 Ensure editor value updation on state change 2022-07-18 21:26:05 +05:30
muzam1l
168d11d48e Remove editorSavedValue from tx state 2022-07-18 19:10:33 +05:30
muzam1l
60f2bb558c Cancel button on json discard dialog. 2022-07-18 17:10:13 +05:30
muzamil
fdf33b9f45 Merge pull request #251 from XRPLF/fix/binary-codec
Fix binary codec (#250)
2022-07-15 16:17:43 +05:30
muzam1l
d05180d148 Fix array fields. 2022-07-15 14:49:54 +05:30
muzamil
bfaa6be17d Fix binary codec (#250)
* TokenID -> NFTokenID

* update xrpl-accountlib

* fixup patch

* add missing transaction types

* fix: repositories

* fix: update @types/react, move middleware

* revert yarn.lock

* revert

* revert: yarn.lock

* fix: transactions

* update NFTokenCancelOffer

* add missing fee

* add: nft transaction type + recalculate `start`

Co-authored-by: dangell7 <denisangell7@gmail.com>
2022-07-15 13:34:19 +05:30
muzamil
9e368dec84 Merge pull request #248 from XRPLF/fix/trust-set
Fix TrustSet transaction example.
2022-07-15 13:13:21 +05:30
muzam1l
25eec6980f Fix TrustSet transaction example. 2022-07-14 20:05:32 +05:30
muzamil
8e2f20c5ac Merge pull request #239 from XRPLF/feat/monaco-comp
Monaco component.
2022-07-14 13:51:24 +05:30
muzam1l
a3d094e873 Fix some more spelling errors. 2022-07-13 20:11:21 +05:30
muzam1l
ef70bfb13a Fix spelling error 2022-07-13 19:49:54 +05:30
muzam1l
c26c7c13d1 Improve compile error handling. 2022-07-13 16:50:47 +05:30
muzam1l
fc461ddd0d Content changed warning on deploy page. 2022-07-13 16:17:31 +05:30
muzam1l
c7001f6089 'Exit editor mode' button in wat editor. 2022-07-13 15:40:08 +05:30
muzam1l
243cbfec08 Upgrade hooks editor to comp. 2022-07-13 15:09:45 +05:30
muzam1l
1295e7fa41 Deploy editor to comp 2022-07-13 15:09:45 +05:30
muzam1l
793623d216 Migrate transaction json editor to comp. 2022-07-13 15:09:45 +05:30
muzam1l
0cde0eb240 Monaco component 2022-07-13 15:09:45 +05:30
muzamil
e2acb48e03 Merge pull request #236 from XRPLF/feat/jsdoc-to-ui
User declaration of input fields via JSDOC in script files.
2022-07-12 16:53:27 +05:30
muzam1l
a4373bb970 fix spelling error 2022-07-12 12:40:35 +05:30
muzam1l
cfb791092a Update template files of some examples to forked versions. 2022-07-11 14:44:35 +05:30
muzamil
3fcbac5ed9 Merge pull request #231 from XRPLF/feat/create-account-name
Allow passing desired name while creating account.
2022-07-11 14:29:38 +05:30
muzam1l
c40b272ce8 Fix disabled prop behaviour 2022-07-07 23:34:14 +05:30
muzam1l
860ff66a8a fix error in error handler 2022-07-07 23:25:53 +05:30
muzam1l
f4f700bea1 Handle required prop on fields. 2022-07-06 19:30:25 +05:30
muzam1l
789bc00ac3 Enhance account secret label! 2022-07-06 19:03:29 +05:30
muzam1l
6a0aabdeda Handle jsdoc errors 2022-07-05 19:38:26 +05:30
muzam1l
175b6266e8 Add script error handling 2022-07-05 19:09:08 +05:30
muzam1l
621482e2ee enhance empty log display 2022-07-05 18:52:45 +05:30
muzam1l
e55f48bc83 Use Single LogBox comp for scripts too 2022-07-05 18:45:52 +05:30
muzam1l
3e9e26a46a Data variables in process.env instead of process 2022-07-05 16:58:07 +05:30
muzam1l
f0e730bb9b Remove secret tag on type Account 2022-07-05 16:35:23 +05:30
muzam1l
6ce4828fc6 Remove console logs 2022-07-05 16:25:15 +05:30
muzam1l
bb0a246ae5 User declaration of input fields via JSDOC in script files. 2022-07-05 16:02:15 +05:30
Valtteri Karesto
3af2bad536 Make ping interval longer (#232)
* Make ping interval 45s

Co-authored-by: Vaclav Barta <vbarta@mangrove.cz>
2022-07-04 12:22:06 +02:00
muzam1l
4f1b877db0 Added optional tag to create account label. 2022-07-01 19:41:52 +05:30
muzamil
0289d64f5e Merge pull request #233 from XRPLF/fix/tab-names
Fix tab names
2022-07-01 19:29:33 +05:30
muzamil
868a0bcf78 Merge pull request #234 from XRPLF/feat/account-in-deploy-dialog
Add account selectable in deploy dialog.
2022-07-01 18:51:04 +05:30
muzam1l
aab2476a05 Add account selectable in deploy dialog. 2022-07-01 18:06:01 +05:30
muzam1l
cb25986d72 update transaction tab labels 2022-07-01 17:30:57 +05:30
muzam1l
309ad57173 Skip auto appneding test file extension. 2022-07-01 17:26:10 +05:30
muzam1l
53afb1d3d1 Fix html erros. 2022-07-01 17:08:34 +05:30
muzam1l
31ff7c0e28 Name field in import account dialog. 2022-07-01 16:43:15 +05:30
muzam1l
dfa35df465 reset input value on submit 2022-07-01 16:27:13 +05:30
muzam1l
f163b052e1 Allow passing desired name while creating account. 2022-07-01 14:33:28 +05:30
Valtteri Karesto
25c5b9c015 Merge pull request #229 from XRPLF/feat/links-to-explorer
Link from hashes/addresses to Hook Explorer
2022-06-30 15:40:57 +03:00
Valtteri Karesto
407e3946ce Added underline on hover to links 2022-06-30 15:30:43 +03:00
Valtteri Karesto
dc5b0d71eb Simplified hook state, since endpoint now works with hookhashes 2022-06-30 08:54:43 +03:00
Valtteri Karesto
3fd6c3f50e Remove debug code 2022-06-29 18:03:26 +03:00
Valtteri Karesto
ec8bfc5eee Add links to account modal 2022-06-29 15:26:33 +03:00
Valtteri Karesto
b4a0bcb90d Merge pull request #227 from XRPLF/feat/remember-deploy-values
Remember deploy values / Add feedback button
2022-06-29 14:08:47 +03:00
Valtteri Karesto
2c729e2aa4 Update button text 2022-06-29 14:04:06 +03:00
Valtteri Karesto
1cb2542170 Merge branch 'main' of github.com:eqlabs/xrpl-hooks-ide into feat/remember-deploy-values 2022-06-29 13:47:41 +03:00
Wietse Wind
00b309df34 Merge pull request #228 from XRPLF/feature/add-plausible-analytics
Add plausible analytics to builder
2022-06-29 12:10:58 +02:00
Joni Juup
a6fc730de6 add plausible analytics to builder 2022-06-29 12:58:17 +03:00
Valtteri Karesto
2245c5a221 Test gh integration 2022-06-29 12:18:38 +03:00
Valtteri Karesto
60c33661ad Add proper defaultvalue 2022-06-29 12:14:52 +03:00
Valtteri Karesto
ea21c85038 Add noopener and noreferrer to link 2022-06-29 11:37:22 +03:00
Valtteri Karesto
5478f43609 persist deploy values in memory 2022-06-29 11:32:15 +03:00
Valtteri Karesto
a9b64abb85 Add feedback button, show modal only on homepage 2022-06-29 11:31:46 +03:00
Valtteri Karesto
c6ced424d8 Merge pull request #226 from XRPLF/feat/long-navigation-support
Fix #215 scrollbar issues
2022-06-28 14:59:35 +03:00
Valtteri Karesto
3a1159cffc Make thumbs more visible 2022-06-28 14:49:34 +03:00
Valtteri Karesto
3136de1bd1 Slight style adjustments 2022-06-28 14:38:59 +03:00
Valtteri Karesto
67ffd3f1b4 Fix #215 scrollbar issues 2022-06-28 14:01:08 +03:00
Valtteri Karesto
8508cb69c4 Merge pull request #224 from XRPLF/feat/debug-stream-fixes
Feat/debug stream fixes
2022-06-28 11:31:03 +03:00
Valtteri Karesto
89217d2633 Remove console.log 2022-06-28 11:03:56 +03:00
Valtteri Karesto
ba1b64391c ping socket connection 2022-06-28 09:36:45 +03:00
Valtteri Karesto
098d919a77 Bring back dispose 2022-06-27 15:03:49 +03:00
Valtteri Karesto
b2af37ab4b Use reconnecting-websocket and refactor debug stream 2022-06-27 15:03:42 +03:00
Valtteri Karesto
dcb7e94e86 New gists provided by XRPL (#223)
* Prepare logic for new gists

* Remove unused imports

* updated gist IDs for xrplfgists

* Add macro.h to apiheaderfiles

* Update headers

Co-authored-by: Vaclav Barta <vbarta@mangrove.cz>
2022-06-27 08:25:25 +02:00
Valtteri Karesto
67848b3d8d Merge pull request #222 from XRPLF/fix/suggest-button-disabled
Fix/suggest button disabled
2022-06-23 11:12:04 +03:00
Valtteri Karesto
31a86263a1 Disable suggest if no account selected 2022-06-22 14:42:50 +03:00
Valtteri Karesto
4d0025afc1 Fix splitscreen error 2022-06-22 14:42:39 +03:00
Valtteri Karesto
f85bd2398d Merge pull request #220 from XRPLF/fix/decimals-again
fixes #201
2022-06-22 12:28:12 +03:00
Valtteri Karesto
a2a6596cc5 Prevent pasting decimals 2022-06-22 12:16:36 +03:00
Valtteri Karesto
37208ce97e fixes #201 2022-06-22 12:03:43 +03:00
Valtteri Karesto
bf4042926d Merge pull request #218 from XRPLF/feat/ui-fixes
Fixes #213 and fixes #200
2022-06-22 11:41:31 +03:00
Valtteri Karesto
3ccc1c16ac Fixed deploy 2022-06-22 11:32:07 +03:00
Valtteri Karesto
135f0c91a1 Fixes #213 and fixes #200 2022-06-22 11:06:15 +03:00
Valtteri Karesto
8f5786e242 Merge pull request #216 from XRPLF/feat/change-default-optimization
fixes #214 change default optimization
2022-06-22 09:56:15 +03:00
Valtteri Karesto
810eb4ca27 Add new default to compileCode as well 2022-06-22 09:52:47 +03:00
Valtteri Karesto
e6574f9f12 fixes #214 change default optimization 2022-06-22 09:47:30 +03:00
Valtteri Karesto
1a6726fabf Merge pull request #212 from XRPLF/feat/improve-supp-js
Improve supplementary JS feature
2022-06-21 11:28:42 +03:00
Valtteri Karesto
89f8671217 Clear log should now work 2022-06-21 10:53:52 +03:00
Valtteri Karesto
fb5259221b Changed color of starting running 2022-06-21 09:43:29 +03:00
Valtteri Karesto
fd17f59616 Show LogBoxForScrips if js file active 2022-06-20 23:54:56 +03:00
Valtteri Karesto
91bbc7ea61 Catch template errors, add better labels, styling 2022-06-20 23:54:33 +03:00
Valtteri Karesto
783d832c6d Remove export for unused component 2022-06-20 23:53:32 +03:00
Valtteri Karesto
698ca376e7 Add showbuttons prop to LogBoxForScripts 2022-06-20 23:53:15 +03:00
Valtteri Karesto
bfd9e21ab8 Remove unused component 2022-06-20 23:52:45 +03:00
Valtteri Karesto
e46411f245 Rename template helpers 2022-06-20 14:53:30 +03:00
Valtteri Karesto
08447c6b29 Add support for select parameters 2022-06-20 14:16:16 +03:00
Valtteri Karesto
9216cc6bf7 When downloading zip, include wasm if it exists 2022-06-20 11:01:13 +03:00
Valtteri Karesto
5108b08e39 Do not show scripts panel if no supplementary scripts 2022-06-20 10:40:47 +03:00
Valtteri Karesto
6c46a4f809 Merge pull request #211 from XRPLF/feat/user-provided-scripts
Feat/user provided scripts
2022-06-20 10:16:06 +03:00
Valtteri Karesto
0ea88f0d32 Remove unused imports 2022-06-17 21:48:32 +03:00
Valtteri Karesto
4c2e1f36f3 Add empty line 2022-06-17 21:42:50 +03:00
Valtteri Karesto
fa5315fc0e Fix errors on development 2022-06-17 17:32:21 +03:00
Valtteri Karesto
eda8b1550c If not string, stringify it 2022-06-17 13:42:19 +03:00
Valtteri Karesto
742b11374f Just empty body 2022-06-16 10:12:40 +03:00
Valtteri Karesto
d16e83dcfa No need for specific log() or error() functions anymore 2022-06-16 00:50:16 +03:00
Valtteri Karesto
155aa57784 Fix warning about useLayoutEffect 2022-06-16 00:49:43 +03:00
Valtteri Karesto
b88b6da7d9 Add one more example env value 2022-06-16 00:49:17 +03:00
Valtteri Karesto
fa13f7e282 Add logic for scripts 2022-06-15 15:15:08 +03:00
Valtteri Karesto
f1a43ef758 Add dependencies and fix handlebars errors 2022-06-15 15:14:51 +03:00
Vaclav Barta
4217813fd7 take default header names from /api/header-files JSON (#210)
This is a client-side upgrade for the server-side XRPLF/xrpl-hooks-compiler#17 - not hard-coding expected header names but taking them from the compilation backend.
2022-06-13 08:22:55 +02:00
muzamil
c588f7b1f3 Merge pull request #209 from XRPLF/feat/json-in-ui
Display tx json in UI inside textarea.
2022-06-10 16:50:28 +05:30
muzam1l
985e8ee820 Merge branch 'main' into feat/json-in-ui 2022-06-10 16:50:08 +05:30
muzamil
8832e76a0a Merge pull request #208 from XRPLF/feat/default-tx
Set payment as default transaction
2022-06-10 16:47:01 +05:30
muzam1l
9777f1dbd1 Increase vert. padding of textarea 2022-06-08 15:03:51 +05:30
muzam1l
213d468aab Merge branch 'main' into feat/json-in-ui 2022-06-07 19:06:57 +05:30
muzam1l
46becb0e7b Display tx json in UI inside textarea. 2022-06-07 19:06:13 +05:30
Valtteri Karesto
fad6bd100a Merge pull request #205 from XRPLF/feat/prevent-decimals
Prevent inputing decimals
2022-06-07 14:55:05 +03:00
muzam1l
5a11f83fea Set payment as default transaction 2022-06-07 00:52:59 +05:30
Valtteri Karesto
525338abf7 Merge branch 'main' of github.com:eqlabs/xrpl-hooks-ide into feat/prevent-decimals 2022-06-03 17:21:00 +03:00
Valtteri Karesto
ea977816a4 Merge pull request #203 from XRPLF/feat/tab-ordering
Sort the files after fetching
2022-06-03 17:20:27 +03:00
Valtteri Karesto
0ee599a2b6 Update sort 2022-06-03 16:50:31 +03:00
Valtteri Karesto
02c59f8d79 Update sorting function 2022-06-03 16:34:46 +03:00
Valtteri Karesto
3d5b77e60a Fix issue if no filenames 2022-06-03 16:34:36 +03:00
Valtteri Karesto
92a167d47a Prevent also pasting 2022-06-03 15:33:46 +03:00
Valtteri Karesto
d41e263942 Merge pull request #204 from XRPLF/feat/remove-cleaner-ui
Remove cleaner options from UI
2022-06-03 15:08:50 +03:00
Valtteri Karesto
bd1226fe90 Remove log 2022-06-02 16:35:18 +03:00
Valtteri Karesto
57403e42dd Adjustments to sorting 2022-06-02 16:34:53 +03:00
Valtteri Karesto
2b42a96c4a Update ordering 2022-06-02 16:15:39 +03:00
Valtteri Karesto
80d6bb691d Prevent inputing decimals 2022-06-02 15:22:28 +03:00
Valtteri Karesto
c7e4cd7c92 Remove unused imports 2022-06-02 14:11:18 +03:00
Valtteri Karesto
4a22861860 Sort the files after fetching 2022-06-02 14:04:52 +03:00
muzamil
b09d029931 Merge pull request #196 from XRPLF/feat/fee-hint
Fee hints in transactions.
2022-06-01 16:21:24 +05:30
muzam1l
b2dc49754f Error toast in suggest button! 2022-06-01 15:51:08 +05:30
muzam1l
6f636645f7 Fix json saving mismatch 2022-05-31 00:53:58 +05:30
muzam1l
377c963c7a FIx json mode schema 2022-05-30 23:32:53 +05:30
muzam1l
ae038f17ff Suggest fee button in transaction ui 2022-05-30 23:01:46 +05:30
Vaclav Barta
0d8f2c31e7 Feature/hook documentation (#197)
* updated hooks-entry-points-check

* added hooks-trivial-cbak

* updated hooks-hash-buf-len: nonce => etxn_nonce + ledger_nonce
2022-05-30 12:49:19 +02:00
muzam1l
da9986eb66 Remove unnecessary console logs 2022-05-27 22:46:15 +05:30
muzam1l
a21350770e Fee hints in transactions. 2022-05-27 16:44:01 +05:30
Valtteri Karesto
49dfd43220 Merge pull request #193 from XRPLF/feat/fee-estimates
Feat/fee estimates
2022-05-27 12:23:50 +03:00
Valtteri Karesto
4472957f5c Updated based on feedback 2022-05-27 10:43:16 +03:00
muzamil
ca46707bb5 Merge pull request #195 from XRPLF/feat/tx_hash_link
Show tx hash instead of server ledger index in deploy log.
2022-05-26 15:28:31 +05:30
muzam1l
704ebe4b92 Show tx hash instead of server ledger index in deploy log. 2022-05-26 14:55:24 +05:30
Valtteri Karesto
9a6ef2c393 Minor fixes based on feedback 2022-05-25 13:39:52 +03:00
Valtteri Karesto
56203ce9c6 Remove package-lock 2022-05-25 13:10:05 +03:00
Valtteri Karesto
933bdb5968 Add fee estimate button to fee and update the deploying 2022-05-25 13:05:04 +03:00
Valtteri Karesto
864711697b Update accounts 2022-05-25 13:03:49 +03:00
Valtteri Karesto
e5eaf09721 Just return the fee values, no mutating 2022-05-25 13:03:38 +03:00
Valtteri Karesto
d0dde56c67 Merge pull request #192 from XRPLF/fix/fix-sequence-number
Fix/fix sequence number
2022-05-24 18:20:31 +03:00
Valtteri Karesto
45c6927e72 Take next sequence number from response 2022-05-24 18:16:56 +03:00
Valtteri Karesto
6014b6e79f Update sequence after successful request 2022-05-24 18:14:01 +03:00
Valtteri Karesto
04a99227df Merge pull request #191 from XRPLF/fix/v2-updates
Fix/v2 updates
2022-05-24 15:44:00 +03:00
Valtteri Karesto
0965a1e898 Merge pull request #190 from XRPLF/feat/update-descriptions
Update meta tags
2022-05-24 15:43:47 +03:00
Valtteri Karesto
32445dbebf Mutate tx with fee estimation 2022-05-24 15:27:20 +03:00
Valtteri Karesto
1a1d4901aa Add estimate fee function 2022-05-24 15:26:38 +03:00
Valtteri Karesto
8b646c56dc Fix recursion 2022-05-24 15:26:03 +03:00
Valtteri Karesto
ac38bbc72c Turn the cleaner on by default 2022-05-24 15:25:33 +03:00
Valtteri Karesto
bf1182351a Added link to project 2022-05-23 13:28:41 +03:00
Valtteri Karesto
55e48a943b Update readme 2022-05-23 13:26:46 +03:00
Valtteri Karesto
faf417be69 Update meta tags 2022-05-23 08:55:03 +03:00
Valtteri Karesto
c2eb57211f hotfix/Remove debug code 2022-05-12 14:31:00 +03:00
Valtteri Karesto
0e97df3c8e Merge pull request #188 from eqlabs/fix/old-account-fix
Fixes to users who has old accounts
2022-05-12 08:35:02 +03:00
Valtteri Karesto
5dd0dfdc18 Fixes to users who has old accounts 2022-05-11 16:07:49 +03:00
Valtteri Karesto
ef48bac8f6 Merge pull request #187 from eqlabs/feat/fix-modal
Updated main modal
2022-05-11 14:31:53 +03:00
Valtteri Karesto
3a3d984098 Updated some copy 2022-05-11 14:22:48 +03:00
Valtteri Karesto
2300c201f8 Lift up modal a bit 2022-05-11 13:37:05 +03:00
Valtteri Karesto
329dc4a355 Remove unused import 2022-05-11 13:31:27 +03:00
Valtteri Karesto
cd6a5b23d4 Icons are now components so we can control the color 2022-05-11 13:30:02 +03:00
Valtteri Karesto
4dd7cbe2ca Updated main modal 2022-05-11 13:07:08 +03:00
Valtteri Karesto
260de7c838 Merge pull request #186 from eqlabs/fix/change-label
Added experimental to label
2022-05-11 10:02:03 +03:00
Valtteri Karesto
e0ed31f220 Added experimental to label 2022-05-11 08:50:00 +03:00
muzamil
eba183497f Merge pull request #176 from eqlabs/feat/json-transactions
Json transactions
2022-05-10 18:15:00 +05:30
Valtteri Karesto
4378afa9a1 Merge pull request #185 from eqlabs/feat/add-cleaner-ui
Feat/add cleaner UI
2022-05-10 14:43:04 +03:00
Valtteri Karesto
491e10920b Updated label 2022-05-10 13:25:41 +03:00
Valtteri Karesto
65bb209713 Fixed wrong key 2022-05-10 11:53:31 +03:00
Valtteri Karesto
c07e70acc9 Add popover descriptions 2022-05-10 11:52:03 +03:00
Valtteri Karesto
8fd7f8ecad Change state key 2022-05-10 11:51:49 +03:00
Valtteri Karesto
2bb3c646db Add compile logic to ui 2022-05-09 14:18:32 +03:00
Valtteri Karesto
87f10a11b0 Add switch component and bg color to popover 2022-05-09 14:18:23 +03:00
Valtteri Karesto
949fb45ae2 Add colors 2022-05-09 14:18:02 +03:00
Valtteri Karesto
ea52f014dd Add radix switch 2022-05-09 14:17:55 +03:00
Valtteri Karesto
77eab8d88d Merge pull request #182 from eqlabs/fix/save-before-sync
"Save" files before syncing to github
2022-05-09 12:56:37 +03:00
Vaclav Barta
4ca8f5f236 Merge pull request #184 from eqlabs/feature/hook-doc-upd
documentation for new checks
2022-05-09 08:36:07 +02:00
muzam1l
53f2a71b08 minor changes 2022-05-05 21:02:33 +05:30
muzam1l
866f6257f1 json schema 2022-05-05 20:00:10 +05:30
Vaclav Barta
814b074cc0 added hooks-control-string-arg, hooks-release-define and hooks-skip-hash-buf-len docs 2022-05-04 14:22:25 +02:00
muzam1l
386619619b support object Amount 2022-05-04 16:57:32 +05:30
muzam1l
d8bf10d0b8 fix handling Destination field 2022-05-04 16:12:50 +05:30
muzam1l
d18c893025 Replace native alerts 2022-05-04 14:38:59 +05:30
Valtteri Karesto
822f7a30f5 "Save" files before syncing to github 2022-05-03 16:33:17 +03:00
Valtteri Karesto
1d66137c23 Merge pull request #181 from eqlabs/feat/add-optimization-settings
Feat/add optimization settings
2022-05-03 15:30:51 +03:00
Valtteri Karesto
4c42e75686 Remove example 2022-05-03 14:46:42 +03:00
Valtteri Karesto
501b7fefec Removed unnecessary setting 2022-05-03 14:17:36 +03:00
Valtteri Karesto
aa7e1517a2 Removed unused import 2022-05-03 14:07:43 +03:00
Valtteri Karesto
e33093f160 Remove console.log 2022-05-03 14:04:18 +03:00
Valtteri Karesto
923b689c98 Add compile options to ui 2022-05-03 14:03:14 +03:00
Valtteri Karesto
246e7f137f Add compile options to compile function 2022-05-03 14:03:02 +03:00
Valtteri Karesto
5defd12a11 Add popover component 2022-05-03 14:02:43 +03:00
Valtteri Karesto
abb7c2bb28 Add compileoptions to global state 2022-05-03 14:02:26 +03:00
Valtteri Karesto
12013907f8 Added radix popover 2022-05-03 14:02:16 +03:00
Valtteri Karesto
ec75fff74b Merge pull request #179 from eqlabs/fix/issue-175
Initial fix for issue #175
2022-05-03 11:20:19 +03:00
muzam1l
5e997044ed alert dialog component 2022-05-02 21:05:18 +05:30
muzam1l
e88720327e Allow using only imported accounts 2022-04-28 18:51:50 +05:30
muzam1l
bf568c3f46 Update button text 2022-04-28 18:33:20 +05:30
Valtteri Karesto
7c1068449f Initial fix for issue #175 2022-04-28 11:46:24 +03:00
Valtteri Karesto
b66d2a09a0 Merge pull request #178 from eqlabs/fix/issue-177
Fixes issue #177
2022-04-28 10:53:26 +03:00
muzam1l
1d3bd128f8 Implement auto save 2022-04-27 23:18:00 +05:30
muzam1l
ab1f45febd Editable json and unified state. 2022-04-27 18:34:28 +05:30
Valtteri Karesto
54265e024c Merge branch 'main' of github.com:eqlabs/xrpl-hooks-ide into fix/issue-177 2022-04-27 15:38:53 +03:00
Valtteri Karesto
20cb66ba18 Fixes issue #177 2022-04-27 15:33:42 +03:00
muzam1l
56a9806b70 add path to tx editor and refactor its value providing 2022-04-25 15:47:58 +05:30
muzam1l
b3f2d0fb6d Readonly tx json view 2022-04-22 18:46:35 +05:30
Vaclav Barta
b7d62dda83 Merge pull request #174 from eqlabs/feat/req-bin-hook-params
requiring user-quoted Hook Parameter values
2022-04-22 13:25:48 +02:00
Vaclav Barta
c690334f92 requiring user-quoted Hook Parameter values 2022-04-21 09:02:59 +02:00
muzamil
587f09ec00 Merge pull request #168 from eqlabs/feat/improved-logs
Background logs.
2022-04-20 16:45:14 +05:30
muzam1l
9296ea1acc remove a console.log 2022-04-20 16:44:49 +05:30
muzam1l
582fb17c94 Link ledger index to explorer 2022-04-20 13:45:17 +05:30
muzam1l
aff0142870 Debug history log only after clear timestamp 2022-04-19 20:51:11 +05:30
muzamil
df51d87cb2 Merge branch 'main' into feat/improved-logs 2022-04-19 20:43:05 +05:30
muzam1l
6a46f5f173 Merge branch 'main' into feat/improved-logs 2022-04-19 18:52:13 +05:30
muzamil
9e25cefef9 Merge pull request #162 from eqlabs/feat/transaction-persistence
Persisted transactions and debug stream state.
2022-04-19 18:42:46 +05:30
muzam1l
95966fa514 Merge branch 'main' into feat/transaction-persistence 2022-04-19 18:38:56 +05:30
Valtteri Karesto
f49d69e75d Merge pull request #172 from eqlabs/fix/regression-#171
Fix/regression #171
2022-04-14 15:41:59 +03:00
Valtteri Karesto
da4b2e68ab Fix issue #171 2022-04-14 15:28:28 +03:00
Valtteri Karesto
5557b1bcba Remove console.logs 2022-04-14 15:27:08 +03:00
Valtteri Karesto
f4b5f98a44 Merge pull request #169 from eqlabs/feat/add-namespace-to-sethook
Add namespace to sethook modal
2022-04-14 15:19:08 +03:00
Valtteri Karesto
b1d39740de Changed labels, added default value to form so the initial render has correct value 2022-04-14 15:10:38 +03:00
muzam1l
dfe5589074 fix 2022-04-14 17:05:36 +05:30
muzam1l
cdc50da840 don't show legacy logs in debug stream 2022-04-14 16:31:19 +05:30
muzam1l
4893b41936 revert to browser time parsing only 2022-04-14 15:02:00 +05:30
Valtteri Karesto
16cbdafb27 Add computed sha256 field to sethook form 2022-04-14 11:52:24 +03:00
Valtteri Karesto
5559fb7be3 rename hash to sha256 2022-04-14 11:51:56 +03:00
Valtteri Karesto
3c4305127b Use user input namespace value 2022-04-13 23:58:02 +03:00
Valtteri Karesto
2a76fa0c35 Add namespace field to set hook modal 2022-04-13 23:57:47 +03:00
muzam1l
bf21fe36c3 timestamp and log fixes 2022-04-13 21:44:24 +05:30
muzam1l
a33a3eb6e2 Background logs 2022-04-13 16:59:02 +05:30
Valtteri Karesto
919c4e173c Merge pull request #166 from eqlabs/feat/remove-peek-from-context-menu
Feat/remove peek from context menu
2022-04-12 09:19:19 +03:00
Valtteri Karesto
650324f434 Hacky way to hide Peek from context menu 2022-04-11 17:07:25 +03:00
Valtteri Karesto
74db96e8a5 add ; 2022-04-11 17:07:13 +03:00
Valtteri Karesto
c99c821081 Update monaco editor and next-themes 2022-04-11 17:07:06 +03:00
Valtteri Karesto
e53a533026 Merge pull request #161 from eqlabs/fix/add-account-creation-error
Fixes issue #68
2022-04-11 14:54:12 +03:00
Valtteri Karesto
5f118e00cb Added error check 2022-04-11 11:46:49 +03:00
muzam1l
46e6927c68 Fix debug stream default account being enforcive 2022-04-08 16:33:46 +05:30
muzam1l
de95a82c5a Fix tx fields not updating 2022-04-08 16:25:03 +05:30
muzam1l
3b66d64c14 Fix debug stream default account when changing tx tabs 2022-04-08 16:03:37 +05:30
muzam1l
6e90a4c3d7 some refactoring 2022-04-08 15:17:54 +05:30
muzam1l
2287e7babb Added Label comp and minor style fixes 2022-04-08 14:41:08 +05:30
muzam1l
c219f7ea00 Persisted transaction tabs state 2022-04-07 16:10:16 +05:30
Valtteri Karesto
e795ce4472 Fixes issue #68 2022-04-06 14:15:53 +03:00
muzam1l
04e2274dbf some refactoring 2022-04-06 14:42:39 +05:30
Valtteri Karesto
6e39b90c1e Merge pull request #160 from eqlabs/feat/fetch-headers-from-api
Fetch header files from c2wasm api
2022-04-06 10:28:51 +03:00
Valtteri Karesto
f186a807c1 Remove unused file 2022-04-06 09:42:03 +03:00
Valtteri Karesto
5ad9ed1688 Fetch header files from c2wasm api 2022-04-05 14:52:19 +03:00
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
Valtteri Karesto
460412d3d7 Button and theme colors changed 2021-11-20 01:34:40 +02:00
Valtteri Karesto
1a182858b4 Added new shade of gray 2021-11-20 01:34:20 +02:00
Valtteri Karesto
baac750e43 Some state management fixes 2021-11-20 01:34:10 +02:00
Valtteri Karesto
74979decbe Merge pull request #29 from eqlabs/feature/empty-pages
Add styles to empty pages
2021-11-19 16:44:24 +02:00
193 changed files with 18288 additions and 2923 deletions

View File

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

5
.gitignore vendored
View File

@@ -32,3 +32,8 @@ yarn-error.log*
# vercel
.vercel
.vscode
# yarn
.yarnrc.yml
.yarn/

38
.prettierignore Normal file
View File

@@ -0,0 +1,38 @@
See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
.vscode
*.md
utils/libwabt.js

8
.prettierrc.json Normal file
View File

@@ -0,0 +1,8 @@
{
"tabWidth": 2,
"arrowParens": "avoid",
"semi": false,
"printWidth": 100,
"singleQuote": true,
"trailingComma": "none"
}

View File

@@ -1,6 +1,8 @@
# XRPL Hooks IDE
# XRPL Hooks Builder
This is the repository for XRPL Hooks IDE. This project is built with Next.JS
https://hooks-builder.xrpl.org/
This is the repository for XRPL Hooks Builder. This project is built with Next.JS
## General
@@ -8,7 +10,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 +28,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:
@@ -33,8 +109,4 @@ To learn more about Next.js, take a look at the following resources:
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.

589
components/Accounts.tsx Normal file
View File

@@ -0,0 +1,589 @@
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, Label } 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'
import { capitalize } from '../utils/helpers'
import { deleteAccount } from '../state/actions/deleteAccount'
import { xrplSend } from '../state/actions/xrpl-client'
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={() => {
deleteAccount(activeAccount?.address)
}}
>
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',
a: { '&:hover': { textDecoration: 'underline' } }
}}
>
<a
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
target="_blank"
rel="noopener noreferrer"
>
{activeAccount?.address}
</a>
</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',
a: { '&:hover': { textDecoration: 'underline' } }
}}
>
{activeAccount && activeAccount.hooks.length > 0
? activeAccount.hooks.map(i => {
return (
<a
key={i}
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${i}`}
target="_blank"
rel="noopener noreferrer"
>
{truncate(i, 12)}
</a>
)
})
: ''}
</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 =>
xrplSend({
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
accountToUpdate.error = null
} else {
const oldAccount = state.accounts.find(acc => acc.address === res?.account)
if (oldAccount) {
oldAccount.xrp = '0'
oldAccount.error = {
code: res?.error,
message: res?.error_message
}
}
}
})
const objectRequests = snap.accounts.map(acc => {
return xrplSend({
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.length, 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' }}>
<ImportAccountDialog type="create" />
<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}{' '}
{!account?.error ? (
`(${Dinero({
amount: Number(account?.xrp || '0'),
precision: 6
})
.toUnit()
.toLocaleString(undefined, {
style: 'currency',
currency: 'XRP',
currencyDisplay: 'name'
})})`
) : (
<Box css={{ color: '$red11' }}>
(Account not found, request funds to activate account)
</Box>
)}
</Text>
</Box>
{!props.hideDeployBtn && (
<div
className="hook-deploy-button"
onClick={e => {
e.stopPropagation()
}}
>
<SetHookDialog accountAddress={account.address} />
</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 = ({ type = 'import' }: { type?: 'import' | 'create' }) => {
const [secret, setSecret] = useState('')
const [name, setName] = useState('')
const btnText = type === 'import' ? 'Import' : 'Create'
const title = type === 'import' ? 'Import Account' : 'Create Account'
const handleSubmit = async () => {
if (type === 'create') {
const value = capitalize(name)
await addFaucetAccount(value, true)
setName('')
setSecret('')
return
}
importAccount(secret, name)
setName('')
setSecret('')
}
return (
<Dialog>
<DialogTrigger asChild>
<Button ghost size="sm">
{btnText}
</Button>
</DialogTrigger>
<DialogContent aria-describedby={undefined}>
<DialogTitle css={{ mb: '$4' }}>{title}</DialogTitle>
<Flex column>
<Box css={{ mb: '$2' }}>
<Label>
Account name <Text muted>(optional)</Text>
</Label>
<Input
name="name"
type="text"
autoComplete="off"
autoCapitalize="on"
value={name}
onChange={e => setName(e.target.value)}
/>
</Box>
{type === 'import' && (
<Box>
<Label>Account secret</Label>
<Input
required
name="secret"
type="password"
autoComplete="new-password"
value={secret}
onChange={e => setSecret(e.target.value)}
/>
</Box>
)}
</Flex>
<Flex
css={{
marginTop: 25,
justifyContent: 'flex-end',
gap: '$3'
}}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button type="submit" variant="primary" onClick={handleSubmit}>
{title}
</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,72 @@
import { FC, ReactNode } from 'react'
import { proxy, useSnapshot } from 'valtio'
import Button from '../Button'
import Flex from '../Flex'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle
} from './primitive'
export interface AlertState {
isOpen: boolean
title?: string
body?: ReactNode
cancelText?: string
confirmText?: string
confirmPrefix?: ReactNode
onConfirm?: () => any
onCancel?: () => any
}
export const alertState = proxy<AlertState>({
isOpen: false
})
const Alert: FC = () => {
const {
title = 'Are you sure?',
isOpen,
body,
cancelText,
confirmText = 'Ok',
confirmPrefix,
onCancel,
onConfirm
} = useSnapshot(alertState)
return (
<AlertDialog open={isOpen} onOpenChange={value => (alertState.isOpen = value)}>
<AlertDialogContent>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{body}</AlertDialogDescription>
<Flex css={{ justifyContent: 'flex-end', gap: '$3' }}>
{(cancelText || onCancel) && (
<AlertDialogCancel asChild>
<Button css={{ minWidth: '$16' }} outline onClick={onCancel}>
{cancelText || 'Cancel'}
</Button>
</AlertDialogCancel>
)}
<AlertDialogAction asChild>
<Button
css={{ minWidth: '$16' }}
variant="primary"
onClick={async () => {
await onConfirm?.()
alertState.isOpen = false
}}
>
{confirmPrefix}
{confirmText}
</Button>
</AlertDialogAction>
</Flex>
</AlertDialogContent>
</AlertDialog>
)
}
export default Alert

View File

@@ -0,0 +1,83 @@
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: '$md'
})
// 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,8 +1,8 @@
import { styled } from "../stitches.config";
import { styled } from '../stitches.config'
const Box = styled("div", {
const Box = styled('div', {
// all: "unset",
boxSizing: "border-box",
});
boxSizing: 'border-box'
})
export default Box;
export default Box

View File

@@ -1,171 +1,285 @@
import { styled } from "../stitches.config";
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",
appereance: "none",
fontFamily: "$body",
alignItems: "center",
boxSizing: "border-box",
userSelect: "none",
"&::before": {
boxSizing: "border-box",
all: 'unset',
position: 'relative',
appereance: 'none',
fontFamily: '$body',
alignItems: 'center',
boxSizing: 'border-box',
userSelect: 'none',
'&::before': {
boxSizing: 'border-box'
},
"&::after": {
boxSizing: "border-box",
'&::after': {
boxSizing: 'border-box'
},
// Custom reset?
display: "inline-flex",
display: 'inline-flex',
flexShrink: 0,
justifyContent: "center",
lineHeight: "1",
gap: "5px",
WebkitTapHighlightColor: "rgba(0,0,0,0)",
justifyContent: 'center',
lineHeight: '1',
gap: '5px',
WebkitTapHighlightColor: 'rgba(0,0,0,0)',
// Custom
height: "$6",
px: "$2",
fontSize: "$2",
height: '$6',
px: '$2',
fontSize: '$2',
fontWeight: 500,
fontVariantNumeric: "tabular-nums",
backgroundColor: "red",
cursor: "pointer",
"&:disabled": {
opacity: 0.8,
pointerEvents: "none",
fontVariantNumeric: 'tabular-nums',
cursor: 'pointer',
width: 'max-content',
'&:disabled': {
opacity: 0.6,
pointerEvents: 'none',
cursor: 'not-allowed'
},
variants: {
size: {
xs: {
borderRadius: '$sm',
height: '$5',
px: '$2',
fontSize: '$xs'
},
sm: {
borderRadius: "$sm",
height: "$7",
px: "$3",
fontSize: "$xs",
borderRadius: '$sm',
height: '$7',
px: '$3',
fontSize: '$xs'
},
md: {
borderRadius: "$sm",
height: "$8",
px: "$3",
fontSize: "$xs",
borderRadius: '$sm',
height: '$8',
px: '$3',
fontSize: '$xs'
},
lg: {
borderRadius: "$sm",
height: "$10",
px: "$4",
fontSize: "$xs",
},
borderRadius: '$sm',
height: '$10',
px: '$4',
fontSize: '$xs'
}
},
variant: {
link: {
textDecoration: 'underline',
fontSize: 'inherit',
color: '$textMuted',
textUnderlineOffset: '2px'
},
default: {
backgroundColor: "$slate12",
boxShadow: "inset 0 0 0 1px $colors$slate12",
color: "$slate1",
"@hover": {
"&:hover": {
backgroundColor: "$slate12",
boxShadow: "inset 0 0 0 1px $colors$slate12",
},
backgroundColor: '$mauve12',
boxShadow: 'inset 0 0 0 1px $colors$mauve12',
color: '$mauve1',
'@hover': {
'&:hover': {
backgroundColor: '$mauve12',
boxShadow: 'inset 0 0 0 1px $colors$mauve12'
}
},
"&:active": {
backgroundColor: "$slate10",
boxShadow: "inset 0 0 0 1px $colors$slate11",
'&:active': {
backgroundColor: '$mauve10',
boxShadow: 'inset 0 0 0 1px $colors$mauve11'
},
"&:focus": {
boxShadow:
"inset 0 0 0 1px $colors$slate12, 0 0 0 1px $colors$slate12",
'&:focus': {
boxShadow: '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"]':
{
backgroundColor: "$slate4",
boxShadow: "inset 0 0 0 1px $colors$slate8",
},
backgroundColor: '$mauve4',
boxShadow: 'inset 0 0 0 1px $colors$mauve8'
}
},
primary: {
backgroundColor: `$pink9`,
boxShadow: "inset 0 0 0 1px $colors$pink9",
color: "$white",
"@hover": {
"&:hover": {
backgroundColor: "$pink10",
boxShadow: "inset 0 0 0 1px $colors$pink11",
},
backgroundColor: `$accent`,
boxShadow: 'inset 0 0 0 1px $colors$purple9',
color: '$white',
'@hover': {
'&:hover': {
backgroundColor: '$purple10',
boxShadow: 'inset 0 0 0 1px $colors$purple11'
}
},
"&:active": {
backgroundColor: "$pink8",
boxShadow: "inset 0 0 0 1px $colors$pink8",
'&:active': {
backgroundColor: '$purple8',
boxShadow: 'inset 0 0 0 1px $colors$purple8'
},
"&:focus": {
boxShadow: "inset 0 0 0 1px $colors$pink8",
'&: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: "$slate4",
boxShadow: "inset 0 0 0 1px $colors$pink8",
},
backgroundColor: '$mauve4',
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: {
backgroundColor: "transparent",
},
backgroundColor: 'transparent'
}
},
uppercase: {
true: {
textTransform: "uppercase",
},
textTransform: 'uppercase'
}
},
fullWidth: {
true: {
width: '100%'
}
},
ghost: {
true: {
boxShadow: "none",
background: "transparent",
color: "$slate12",
"@hover": {
"&:hover": {
backgroundColor: "$slate6",
boxShadow: "none",
},
boxShadow: 'none',
background: 'transparent',
color: '$mauve12',
'@hover': {
'&:hover': {
backgroundColor: '$mauve6',
boxShadow: 'none'
}
},
"&:active": {
backgroundColor: "$slate8",
boxShadow: "none",
'&:active': {
backgroundColor: '$mauve8',
boxShadow: 'none'
},
"&:focus": {
boxShadow: "none",
},
},
'&:focus': {
boxShadow: 'none'
}
}
},
isLoading: {
true: {
'& .button-content': {
visibility: 'hidden'
},
pointerEvents: 'none'
}
}
},
compoundVariants: [
{
outline: true,
variant: "default",
variant: 'default',
css: {
background: "transparent",
color: "$slate12",
boxShadow: "inset 0 0 0 1px $colors$slate10",
"&:hover": {
color: "$slate12",
background: "$slate5",
},
},
background: 'transparent',
color: '$mauve12',
boxShadow: 'inset 0 0 0 1px $colors$mauve10',
'&:hover': {
color: '$mauve12',
background: '$mauve5'
}
}
},
{
outline: true,
variant: "primary",
variant: 'primary',
css: {
background: "transparent",
color: "$slate12",
"&:hover": {
color: "$slate12",
background: "$slate5",
},
},
background: 'transparent',
color: '$mauve12',
'&:hover': {
color: '$mauve12',
background: '$mauve5'
}
}
},
{
outline: true,
variant: 'secondary',
css: {
background: 'transparent',
color: '$mauve12',
'&:hover': {
color: '$mauve12',
background: '$mauve5'
}
}
}
],
defaultVariants: {
size: "md",
variant: "default",
},
});
size: 'md',
variant: 'default'
}
})
export default Button;
const CustomButton: React.FC<React.ComponentProps<typeof StyledButton> & { as?: string }> =
React.forwardRef(({ children, as = 'button', ...rest }, ref) => (
// @ts-expect-error
<StyledButton {...rest} ref={ref} as={as}>
<Flex as="span" css={{ gap: '$2', alignItems: 'center' }} className="button-content">
{children}
</Flex>
{rest.isLoading && <Spinner css={{ position: 'absolute' }} />}
</StyledButton>
))
CustomButton.displayName = 'CustomButton'
export default 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

View File

@@ -1,12 +1,12 @@
import { styled } from "../stitches.config";
import Box from "./Box";
import { styled } from '../stitches.config'
import Box from './Box'
const Container = styled(Box, {
width: "100%",
marginLeft: "auto",
marginRight: "auto",
px: "$4",
maxWidth: "100%",
});
width: '100%',
marginLeft: 'auto',
marginRight: 'auto',
px: '$4',
maxWidth: '100%'
})
export default Container;
export default Container

View File

@@ -0,0 +1,121 @@
import { CaretRight, Check, Circle } from 'phosphor-react'
import { FC, Fragment, ReactNode } from 'react'
import { Flex, Text } from '../'
import {
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuItem,
ContextMenuItemIndicator,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuRoot,
ContextMenuSeparator,
ContextMenuTrigger,
ContextMenuTriggerItem
} from './primitive'
export type TextOption = {
type: 'text'
label: ReactNode
onSelect?: () => any
children?: ContentMenuOption[]
}
export type SeparatorOption = { type: 'separator' }
export type CheckboxOption = {
type: 'checkbox'
label: ReactNode
checked?: boolean
onCheckedChange?: (isChecked: boolean) => any
}
export type RadioOption<T extends string = string> = {
type: 'radio'
label: ReactNode
onValueChange?: (value: string) => any
value: T
options?: { value: T; label?: ReactNode }[]
}
type WithCommons = { key: string; disabled?: boolean }
export type ContentMenuOption = (TextOption | SeparatorOption | CheckboxOption | RadioOption) &
WithCommons
export interface IContextMenu {
options?: ContentMenuOption[]
isNested?: boolean
}
export const ContextMenu: FC<IContextMenu> = ({ children, options, isNested }) => {
return (
<ContextMenuRoot>
{isNested ? (
<ContextMenuTriggerItem>{children}</ContextMenuTriggerItem>
) : (
<ContextMenuTrigger>{children}</ContextMenuTrigger>
)}
{options && !!options.length && (
<ContextMenuContent sideOffset={isNested ? 2 : 5}>
{options.map(({ key, ...option }) => {
if (option.type === 'text') {
const { children, label, onSelect } = option
if (children)
return (
<ContextMenu isNested key={key} options={children}>
<Flex fluid row justify="space-between" align="center">
<Text>{label}</Text>
<CaretRight />
</Flex>
</ContextMenu>
)
return (
<ContextMenuItem key={key} onSelect={onSelect}>
{label}
</ContextMenuItem>
)
}
if (option.type === 'checkbox') {
const { label, checked, onCheckedChange } = option
return (
<ContextMenuCheckboxItem
key={key}
checked={checked}
onCheckedChange={onCheckedChange}
>
<Flex row align="center">
<ContextMenuItemIndicator>
<Check />
</ContextMenuItemIndicator>
<Text css={{ ml: checked ? '$4' : undefined }}>{label}</Text>
</Flex>
</ContextMenuCheckboxItem>
)
}
if (option.type === 'radio') {
const { label, options, onValueChange, value } = option
return (
<Fragment key={key}>
<ContextMenuLabel>{label}</ContextMenuLabel>
<ContextMenuRadioGroup value={value} onValueChange={onValueChange}>
{options?.map(({ value: v, label }) => {
return (
<ContextMenuRadioItem key={v} value={v}>
<ContextMenuItemIndicator>
<Circle weight="fill" />
</ContextMenuItemIndicator>
<Text css={{ ml: '$4' }}>{label}</Text>
</ContextMenuRadioItem>
)
})}
</ContextMenuRadioGroup>
</Fragment>
)
}
return <ContextMenuSeparator key={key} />
})}
</ContextMenuContent>
)}
</ContextMenuRoot>
)
}
export default ContextMenu

View File

@@ -0,0 +1,107 @@
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu'
import { styled } from '../../stitches.config'
import {
slideDownAndFade,
slideLeftAndFade,
slideRightAndFade,
slideUpAndFade
} from '../../styles/keyframes'
const StyledContent = styled(ContextMenuPrimitive.Content, {
minWidth: 140,
backgroundColor: '$backgroundOverlay',
borderRadius: 6,
overflow: 'hidden',
padding: '5px',
boxShadow:
'0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)',
'@media (prefers-reduced-motion: no-preference)': {
animationDuration: '400ms',
animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
willChange: 'transform, opacity',
'&[data-state="open"]': {
'&[data-side="top"]': { animationName: slideDownAndFade },
'&[data-side="right"]': { animationName: slideLeftAndFade },
'&[data-side="bottom"]': { animationName: slideUpAndFade },
'&[data-side="left"]': { animationName: slideRightAndFade }
}
},
'.dark &': {
boxShadow:
'0px 10px 38px -10px rgba(22, 23, 24, 0.85), 0px 10px 20px -15px rgba(22, 23, 24, 0.6)'
}
})
const itemStyles = {
all: 'unset',
fontSize: 13,
lineHeight: 1,
color: '$text',
borderRadius: 3,
display: 'flex',
alignItems: 'center',
height: 28,
padding: '0 7px',
position: 'relative',
paddingLeft: 10,
userSelect: 'none',
'&[data-disabled]': {
color: '$textMuted',
pointerEvents: 'none'
},
'&:focus': {
backgroundColor: '$purple9',
color: '$white'
}
}
const StyledItem = styled(ContextMenuPrimitive.Item, { ...itemStyles })
const StyledCheckboxItem = styled(ContextMenuPrimitive.CheckboxItem, {
...itemStyles
})
const StyledRadioItem = styled(ContextMenuPrimitive.RadioItem, {
...itemStyles
})
const StyledTriggerItem = styled(ContextMenuPrimitive.TriggerItem, {
'&[data-state="open"]': {
backgroundColor: '$purple9',
color: '$purple9'
},
...itemStyles
})
const StyledLabel = styled(ContextMenuPrimitive.Label, {
paddingLeft: 10,
fontSize: 12,
lineHeight: '25px',
color: '$text'
})
const StyledSeparator = styled(ContextMenuPrimitive.Separator, {
height: 1,
backgroundColor: '$backgroundAlt',
margin: 5
})
const StyledItemIndicator = styled(ContextMenuPrimitive.ItemIndicator, {
position: 'absolute',
left: 0,
width: 25,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center'
})
export const ContextMenuRoot = ContextMenuPrimitive.Root
export const ContextMenuTrigger = ContextMenuPrimitive.Trigger
export const ContextMenuContent = StyledContent
export const ContextMenuItem = StyledItem
export const ContextMenuCheckboxItem = StyledCheckboxItem
export const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
export const ContextMenuRadioItem = StyledRadioItem
export const ContextMenuItemIndicator = StyledItemIndicator
export const ContextMenuTriggerItem = StyledTriggerItem
export const ContextMenuLabel = StyledLabel
export const ContextMenuSeparator = StyledSeparator

182
components/DebugStream.tsx Normal file
View File

@@ -0,0 +1,182 @@
import { useEffect } from 'react'
import ReconnectingWebSocket, { CloseEvent } from 'reconnecting-websocket'
import { proxy, ref, useSnapshot } from 'valtio'
import { subscribeKey } from 'valtio/utils'
import { Select } from '.'
import state, { ILog, transactionsState } from '../state'
import { extractJSON } from '../utils/json'
import EnrichLog from './EnrichLog'
import LogBox from './LogBox'
interface ISelect<T = string> {
label: string
value: T
}
export interface IStreamState {
selectedAccount: ISelect | null
status: 'idle' | 'opened' | 'closed'
statusChangeTimestamp?: number
logs: ILog[]
socket?: ReconnectingWebSocket
}
export const streamState = proxy<IStreamState>({
selectedAccount: null as ISelect | null,
status: 'idle',
logs: [] as ILog[]
})
const onOpen = (account: ISelect | null) => {
if (!account) {
return
}
// streamState.logs = [];
streamState.status = 'opened'
streamState.statusChangeTimestamp = Date.now()
pushLog(`Debug stream opened for account ${account?.value}`, {
type: 'success'
})
}
const onError = () => {
pushLog('Something went wrong! Check your connection and try again.', {
type: 'error'
})
}
const onClose = (e: CloseEvent) => {
// 999 = closed websocket connection by switching account
if (e.code !== 4999) {
pushLog(`Connection was closed. [code: ${e.code}]`, {
type: 'error'
})
}
streamState.status = 'closed'
streamState.statusChangeTimestamp = Date.now()
}
const onMessage = (event: any) => {
// Ping returns just account address, if we get that
// response we don't need to log anything
if (event.data !== streamState.selectedAccount?.value) {
pushLog(event.data)
}
}
let interval: NodeJS.Timer | null = null
const addListeners = (account: ISelect | null) => {
if (account?.value && streamState.socket?.url.endsWith(account?.value)) {
return
}
streamState.logs = []
if (account?.value) {
if (interval) {
clearInterval(interval)
}
if (streamState.socket) {
streamState.socket?.removeEventListener('open', () => onOpen(account))
streamState.socket?.removeEventListener('close', onClose)
streamState.socket?.removeEventListener('error', onError)
streamState.socket?.removeEventListener('message', onMessage)
}
streamState.socket = ref(
new ReconnectingWebSocket(
`wss://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/${account?.value}`
)
)
if (streamState.socket) {
interval = setInterval(() => {
streamState.socket?.send('')
}, 45000)
}
streamState.socket.addEventListener('open', () => onOpen(account))
streamState.socket.addEventListener('close', onClose)
streamState.socket.addEventListener('error', onError)
streamState.socket.addEventListener('message', onMessage)
}
}
subscribeKey(streamState, 'selectedAccount', addListeners)
const clearLog = () => {
streamState.logs = []
streamState.statusChangeTimestamp = Date.now()
}
const DebugStream = () => {
const { selectedAccount, logs } = useSnapshot(streamState)
const { activeHeader: activeTxTab } = useSnapshot(transactionsState)
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.socket?.close(4999, 'Old connection closed because user switched account')
streamState.selectedAccount = acc as any
}}
css={{ width: '100%' }}
/>
</>
)
useEffect(() => {
const account = transactionsState.transactions.find(tx => tx.header === activeTxTab)?.state
.selectedAccount
if (account && account.value !== streamState.selectedAccount?.value)
streamState.selectedAccount = account
}, [activeTxTab])
return (
<LogBox enhanced renderNav={renderNav} title="Debug stream" logs={logs} clearLog={clearLog} />
)
}
export default DebugStream
export const pushLog = (str: any, opts: Partial<Pick<ILog, 'type'>> = {}): ILog | undefined => {
if (!str) return
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 timestamp = Date.parse(tm || '') || undefined
const timestring = !timestamp ? tm : new Date(timestamp).toLocaleTimeString()
const extracted = extractJSON(msg)
const _message = !extracted ? msg : msg.slice(0, extracted.start) + msg.slice(extracted.end + 1)
const message = ref(<EnrichLog str={_message} />)
const _jsonData = extracted ? JSON.stringify(extracted.result, null, 2) : undefined
const jsonData = _jsonData ? ref(<EnrichLog str={_jsonData} />) : undefined
if (extracted?.result?.id?._Request?.includes('hooks-builder-req')) {
return
}
const { type = 'log' } = opts
const log: ILog = {
type,
message,
timestring,
jsonData,
defaultCollapsed: true
}
if (log) streamState.logs.push(log)
return log
}

166
components/DeployEditor.tsx Normal file
View File

@@ -0,0 +1,166 @@
import React, { useState } from 'react'
import { useSnapshot } from 'valtio'
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 state from '../state'
import wat from '../utils/wat-highlight'
import EditorNavigation from './EditorNavigation'
import { Button, Text, Link, Flex, Tabs, Tab } from '.'
import Monaco from './Monaco'
const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024]
const DeployEditor = () => {
const snap = useSnapshot(state)
const router = useRouter()
const { theme } = useTheme()
const [showContent, setShowContent] = useState(false)
const compiledFiles = snap.files.filter(file => file.compiledContent)
const activeFile = compiledFiles[snap.activeWat]
const renderNav = () => (
<Tabs activeIndex={snap.activeWat} onChangeActive={idx => (state.activeWat = idx)}>
{compiledFiles.map((file, index) => {
return <Tab key={file.name} header={`${file.name}.wat`} />
})}
</Tabs>
)
const compiledSize = activeFile?.compiledContent?.byteLength || 0
const color =
compiledSize > FILESIZE_BREAKPOINTS[1]
? '$error'
: compiledSize > FILESIZE_BREAKPOINTS[0]
? '$warning'
: '$success'
const isContentChanged = activeFile && activeFile.compiledValueSnapshot !== activeFile.content
// const hasDeployErrors = activeFile && activeFile.containsErrors;
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>
{activeFile.compiledContent?.byteLength && activeFile.compiledContent?.byteLength >= 64000 && (
<Flex css={{ flexDirection: 'column', py: '$3', pb: '$1' }}>
<Text css={{ ml: '$2', color: '$error' }}>
File size is larger than 64kB, cannot set hook!
</Text>
</Flex>
)}
<Button variant="link" onClick={() => setShowContent(true)}>
View as WAT-file
</Button>
{isContentChanged && (
<Text warning>
File contents were changed after last compile, compile again to incorporate your latest
changes in the build.
</Text>
)}
</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 renderNav={renderNav} />
<Container
css={{
display: 'flex',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}}
>
{!isContent ? (
NoContentView
) : !showContent ? (
CompiledStatView
) : (
<Monaco
className="hooks-editor"
defaultLanguage={'wat'}
language={'wat'}
path={`file://tmp/c/${activeFile?.name}.wat`}
value={activeFile?.compiledWatContent || ''}
beforeMount={monaco => {
monaco.languages.register({ id: 'wat' })
monaco.languages.setLanguageConfiguration('wat', wat.config)
monaco.languages.setMonarchTokensProvider('wat', wat.tokens)
}}
onMount={editor => {
editor.updateOptions({
glyphMargin: true,
readOnly: true
})
}}
theme={theme === 'dark' ? 'dark' : 'light'}
overlay={
<Flex
css={{
m: '$1',
ml: 'auto',
fontSize: '$sm',
color: '$textMuted'
}}
>
<Link onClick={() => setShowContent(false)}>Exit editor mode</Link>
</Flex>
}
/>
)}
</Container>
</Box>
)
}
export default DeployEditor

View File

@@ -1,103 +1,88 @@
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 * as DialogPrimitive from "@radix-ui/react-dialog";
import { styled } from "../stitches.config";
import React from 'react'
import * as Stiches from '@stitches/react'
import { keyframes } from '@stitches/react'
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 },
"100%": { opacity: 1 },
});
'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: 3000,
backgroundColor: blackA.blackA9,
position: "fixed",
position: 'fixed',
inset: 0,
"@media (prefers-reduced-motion: no-preference)": {
animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'grid',
placeItems: 'center',
overflowY: 'auto',
'@media (prefers-reduced-motion: no-preference)': {
animation: `${overlayShow} 250ms cubic-bezier(0.16, 1, 0.3, 1)`
},
".dark &": {
backgroundColor: blackA.blackA9,
},
});
'.dark &': {
backgroundColor: blackA.blackA11
}
})
const StyledContent = styled(DialogPrimitive.Content, {
zIndex: 1000,
backgroundColor: "$slate2",
color: "$slate12",
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",
backgroundColor: '$mauve2',
color: '$mauve12',
borderRadius: '$md',
position: 'relative',
mb: '15%',
boxShadow: '0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)',
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)`,
'@media (prefers-reduced-motion: no-preference)': {
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`
},
"&:focus": { outline: "none" },
".dark &": {
backgroundColor: "$slate5",
boxShadow:
"0px 10px 38px 0px rgba(22, 23, 24, 0.85), 0px 10px 20px 0px rgba(22, 23, 24, 0.6)",
},
});
'&: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 Content: React.FC<{ css?: Stiches.CSS }> = ({ css, children }) => {
return (
<div>
<StyledOverlay />
<StyledOverlay>
<StyledContent css={css}>{children}</StyledContent>
</div>
);
};
</StyledOverlay>
)
}
const StyledTitle = styled(DialogPrimitive.Title, {
margin: 0,
fontWeight: 500,
color: "$slate12",
fontSize: 17,
});
color: '$mauve12',
fontSize: 17
})
const StyledDescription = styled(DialogPrimitive.Description, {
margin: "10px 0 20px",
color: "$slate11",
margin: '10px 0 10px',
color: '$mauve11',
fontSize: 15,
lineHeight: 1.5,
});
lineHeight: 1.5
})
// Exports
export const Dialog = DialogPrimitive.Root;
export const DialogTrigger = DialogPrimitive.Trigger;
export const DialogContent = Content;
export const DialogTitle = StyledTitle;
export const DialogDescription = StyledDescription;
export const DialogClose = DialogPrimitive.Close;
const Input = styled("input", {
all: "unset",
width: "100%",
flex: "1",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 4,
padding: "0 10px",
fontSize: 15,
lineHeight: 1,
color: violet.violet11,
boxShadow: `0 0 0 1px ${violet.violet7}`,
height: 35,
"&:focus": { boxShadow: `0 0 0 2px ${violet.violet8}` },
});
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

@@ -1,131 +1,120 @@
import { keyframes } from "@stitches/react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { styled } from "../stitches.config";
import { blackA, slateDark } from "@radix-ui/colors";
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)" },
});
import { styled } from '../stitches.config'
import {
slideDownAndFade,
slideLeftAndFade,
slideRightAndFade,
slideUpAndFade
} from '../styles/keyframes'
const StyledContent = styled(DropdownMenuPrimitive.Content, {
minWidth: 220,
backgroundColor: "$slate2",
backgroundColor: '$mauve2',
borderRadius: 6,
padding: 5,
boxShadow:
"0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)",
"@media (prefers-reduced-motion: no-preference)": {
animationDuration: "400ms",
animationTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
willChange: "transform, opacity",
'0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)',
'@media (prefers-reduced-motion: no-preference)': {
animationDuration: '400ms',
animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
willChange: 'transform, opacity',
'&[data-state="open"]': {
'&[data-side="top"]': { animationName: slideDownAndFade },
'&[data-side="right"]': { animationName: slideLeftAndFade },
'&[data-side="bottom"]': { animationName: slideUpAndFade },
'&[data-side="left"]': { animationName: slideRightAndFade },
},
'&[data-side="left"]': { animationName: slideRightAndFade }
}
},
".dark &": {
backgroundColor: "$slate5",
'.dark &': {
backgroundColor: '$mauve5',
boxShadow:
"0px 10px 38px -10px rgba(22, 23, 24, 0.85), 0px 10px 20px -15px rgba(22, 23, 24, 0.6)",
},
});
'0px 10px 38px -10px rgba(22, 23, 24, 0.85), 0px 10px 20px -15px rgba(22, 23, 24, 0.6)'
}
})
const itemStyles = {
all: "unset",
all: 'unset',
fontSize: 13,
lineHeight: 1,
color: "$slate12",
color: '$mauve12',
borderRadius: 3,
display: "flex",
alignItems: "center",
display: 'flex',
alignItems: 'center',
height: 32,
padding: "0 5px",
position: "relative",
paddingLeft: "5px",
userSelect: "none",
gap: "$2",
padding: '0 5px',
position: 'relative',
paddingLeft: '5px',
userSelect: 'none',
py: '$0.5',
pr: '$2',
gap: '$2',
"&[data-disabled]": {
color: "$slate9",
pointerEvents: "none",
'&[data-disabled]': {
color: '$mauve9',
pointerEvents: 'none'
},
"&:focus": {
backgroundColor: "$pink9",
color: "$white",
},
};
'&:focus': {
backgroundColor: '$purple9',
color: '$white'
}
}
const StyledItem = styled(DropdownMenuPrimitive.Item, { ...itemStyles });
const StyledItem = styled(DropdownMenuPrimitive.Item, { ...itemStyles })
const StyledCheckboxItem = styled(DropdownMenuPrimitive.CheckboxItem, {
...itemStyles,
});
...itemStyles
})
const StyledRadioItem = styled(DropdownMenuPrimitive.RadioItem, {
...itemStyles,
});
...itemStyles
})
const StyledTriggerItem = styled(DropdownMenuPrimitive.TriggerItem, {
'&[data-state="open"]': {
backgroundColor: "$pink9",
color: "$pink9",
backgroundColor: '$purple9',
color: '$purple9'
},
...itemStyles,
});
...itemStyles
})
const StyledLabel = styled(DropdownMenuPrimitive.Label, {
paddingLeft: 25,
fontSize: 12,
lineHeight: "25px",
color: "$slate11",
});
lineHeight: '25px',
color: '$mauve11'
})
const StyledSeparator = styled(DropdownMenuPrimitive.Separator, {
height: 1,
backgroundColor: "$slate7",
margin: 5,
});
backgroundColor: '$mauve7',
margin: 5
})
const StyledItemIndicator = styled(DropdownMenuPrimitive.ItemIndicator, {
position: "absolute",
position: 'absolute',
left: 0,
width: 25,
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
});
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center'
})
const StyledArrow = styled(DropdownMenuPrimitive.Arrow, {
fill: "$slate2",
});
fill: '$mauve2',
'.dark &': {
fill: '$mauve5'
}
})
// Exports
export const DropdownMenu = DropdownMenuPrimitive.Root;
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
export const DropdownMenuContent = StyledContent;
export const DropdownMenuItem = StyledItem;
export const DropdownMenuCheckboxItem = StyledCheckboxItem;
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
export const DropdownMenuRadioItem = StyledRadioItem;
export const DropdownMenuItemIndicator = StyledItemIndicator;
export const DropdownMenuTriggerItem = StyledTriggerItem;
export const DropdownMenuLabel = StyledLabel;
export const DropdownMenuSeparator = StyledSeparator;
export const DropdownMenuArrow = StyledArrow;
export const DropdownMenu = DropdownMenuPrimitive.Root
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
export const DropdownMenuContent = StyledContent
export const DropdownMenuItem = StyledItem
export const DropdownMenuCheckboxItem = StyledCheckboxItem
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
export const DropdownMenuRadioItem = StyledRadioItem
export const DropdownMenuItemIndicator = StyledItemIndicator
export const DropdownMenuTriggerItem = StyledTriggerItem
export const DropdownMenuLabel = StyledLabel
export const DropdownMenuSeparator = StyledSeparator
export const DropdownMenuArrow = StyledArrow

View File

@@ -1,234 +1,379 @@
import React, { useRef, useState } from "react";
import { Plus, Share, DownloadSimple, Gear, X } from "phosphor-react";
import { useTheme } from "next-themes";
import React, { useState, useEffect, useRef, ReactNode } from 'react'
import {
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 Box from "./Box";
import Button from "./Button";
import Container from "./Container";
import { syncToGist, updateEditorSettings, downloadAsZip } from '../state/actions'
import state from '../state'
import Box from './Box'
import Button from './Button'
import Container from './Container'
import {
Dialog,
DialogTrigger,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose,
} 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";
DialogClose
} from './Dialog'
import Flex from './Flex'
import Stack from './Stack'
import { Input, Label } from './Input'
import Tooltip from './Tooltip'
import { showAlert } from '../state/actions/showAlert'
const EditorNavigation = () => {
const snap = useSnapshot(state);
const [filename, setFilename] = useState("");
const { theme } = useTheme();
const { data: session, status } = useSession();
const router = useRouter();
const [editorSettings, setEditorSettings] = useState(snap.editorSettings);
const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
const snap = useSnapshot(state)
const [editorSettingsOpen, setEditorSettingsOpen] = useState(false)
const { data: session, status } = useSession()
const [popup, setPopUp] = useState(false)
const [editorSettings, setEditorSettings] = useState(snap.editorSettings)
useEffect(() => {
if (session && session.user && popup) {
setPopUp(false)
}
}, [session, popup])
const showNewGistAlert = () => {
showAlert('Are you sure?', {
body: (
<>
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.
</>
),
cancelText: 'Cancel',
confirmText: 'Create new Gist',
confirmPrefix: <FilePlus size="15px" />,
onConfirm: () => syncToGist(session, true)
})
}
const scrollRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
return (
<Flex css={{ flexShrink: 0, gap: "$0" }}>
<Flex css={{ overflowX: "scroll", py: "$3", flex: 1 }}>
<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}
css={{
"&:hover": {
span: {
visibility: "visible",
},
},
}}
>
{file.name}
<Box
as="span"
css={{
display: "flex",
p: "1px",
borderRadius: "$full",
mr: "-4px",
}}
onClick={() => state.files.splice(index, 1)}
>
<X size="13px" />
</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>
<span>
Create empty C file or select one of the existing ones
</span>
<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("");
}}
>
Create file
</Button>
</DialogClose>
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
</Stack>
<Flex css={{ flexShrink: 0, gap: '$0' }}>
<Flex
id="kissa"
ref={scrollRef}
css={{
overflowX: 'scroll',
overflowY: 'hidden',
py: '$3',
pb: '$0',
flex: 1,
'&::-webkit-scrollbar': {
height: '0.3em',
background: 'rgba(0,0,0,.0)'
},
'&::-webkit-scrollbar-gutter': 'stable',
'&::-webkit-scrollbar-thumb': {
backgroundColor: 'rgba(0,0,0,.2)',
outline: '0px',
borderRadius: '9999px'
},
scrollbarColor: 'rgba(0,0,0,.2) rgba(0,0,0,0)',
scrollbarGutter: 'stable',
scrollbarWidth: 'thin',
'.dark &': {
'&::-webkit-scrollbar': {
background: 'rgba(0,0,0,.0)'
},
'&::-webkit-scrollbar-gutter': 'stable',
'&::-webkit-scrollbar-thumb': {
backgroundColor: 'rgba(255,255,255,.2)',
outline: '0px',
borderRadius: '9999px'
},
scrollbarColor: 'rgba(255,255,255,.2) rgba(0,0,0,0)',
scrollbarGutter: 'stable',
scrollbarWidth: 'thin'
}
}}
onWheelCapture={e => {
if (scrollRef.current) {
scrollRef.current.scrollLeft += e.deltaY
}
}}
>
<Container css={{ flex: 1 }} ref={containerRef}>
{renderNav?.()}
</Container>
</Flex>
<Flex
css={{
py: "$3",
backgroundColor: "$slate3",
zIndex: 1,
py: '$3',
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",
display: 'inline-flex',
marginLeft: 'auto',
flexShrink: 0,
gap: "$0",
border: "1px solid $slate10",
borderRadius: "$sm",
gap: '$0',
borderRadius: '$sm',
boxShadow: 'inset 0px 0px 0px 1px $colors$mauve10',
zIndex: 9,
position: "relative",
overflow: "hidden",
position: 'relative',
button: {
borderRadius: "$0",
px: "$2",
alignSelf: "flex-start",
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 {
showNewGistAlert()
}
}}
>
{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>
<span>You can edit your editor settings here</span>
<input
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={() => {
showNewGistAlert()
}}
>
<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>
</Flex>
);
};
export default EditorNavigation;
<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>
)
}
export default EditorNavigation

73
components/EnrichLog.tsx Normal file
View File

@@ -0,0 +1,73 @@
import { FC, useState } from 'react'
import regexifyString from 'regexify-string'
import { useSnapshot } from 'valtio'
import { Link } from '.'
import state from '../state'
import { AccountDialog } from './Accounts'
import Tooltip from './Tooltip'
import hookSetCodes from '../content/hook-set-codes.json'
import { capitalize } from '../utils/helpers'
interface EnrichLogProps {
str?: string
}
const EnrichLog: FC<EnrichLogProps> = ({ str }) => {
const { accounts } = useSnapshot(state)
const [dialogAccount, setDialogAccount] = useState<string | null>(null)
if (!str || !accounts.length) return <>{str}</>
const addrs = accounts.map(acc => acc.address)
const regex = `(${addrs.join('|')}|HookSet\\(\\d+\\))`
const res = regexifyString({
pattern: new RegExp(regex, 'gim'),
decorator: (match, idx) => {
if (match.startsWith('r')) {
// Account
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>
)
}
if (match.startsWith('HookSet')) {
const code = match.match(/^HookSet\((\d+)\)/)?.[1]
const val = hookSetCodes.find(v => code && v.code === +code)
console.log({ code, val })
if (!val) return match
const content = capitalize(val.description) || 'No hint available!'
return (
<>
HookSet(
<Tooltip content={content}>
<Link>{val.identifier}</Link>
</Tooltip>
)
</>
)
}
return match
},
input: str
})
return (
<>
{res}
<AccountDialog
setActiveAccountAddress={setDialogAccount}
activeAccountAddress={dialogAccount}
/>
</>
)
}
export default EnrichLog

View File

@@ -1,8 +1,53 @@
import { styled } from "../stitches.config";
import Box from "./Box";
import { styled } from '../stitches.config'
import Box from './Box'
const Flex = styled(Box, {
display: "flex",
});
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;
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 $slate6",
background: "$slate1",
}}
>
<Container css={{ py: "$4", 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 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

@@ -1,10 +1,16 @@
import { styled } from "../stitches.config";
import { styled } from '../stitches.config'
const Heading = styled("span", {
fontFamily: "$heading",
lineHeight: "$heading",
fontWeight: "$heading",
textTransform: "uppercase",
});
const Heading = styled('span', {
fontFamily: '$heading',
lineHeight: '$heading',
fontWeight: '$heading',
variants: {
uppercase: {
true: {
textTransform: 'uppercase'
}
}
}
})
export default Heading;
export default Heading

View File

@@ -1,98 +1,333 @@
import React, { useEffect, useRef } from "react";
import { useSnapshot, ref } from "valtio";
import Editor from "@monaco-editor/react";
import type monaco from "monaco-editor";
import { Play } from "phosphor-react";
import { useTheme } from "next-themes";
import React, { useEffect, useRef, useState } from 'react'
import { useSnapshot, ref } from 'valtio'
import type monaco from 'monaco-editor'
import { ArrowBendLeftUp } from 'phosphor-react'
import { useTheme } from 'next-themes'
import { useRouter } from 'next/router'
import Box from "./Box";
import Button from "./Button";
import dark from "../theme/editor/amy.json";
import light from "../theme/editor/xcode_default.json";
import { compileCode, saveFile, state } from "../state";
import Box from './Box'
import Container from './Container'
import { createNewFile, saveFile } from '../state/actions'
import { apiHeaderFiles } from '../state/constants'
import state from '../state'
import EditorNavigation from "./EditorNavigation";
import Spinner from "./Spinner";
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'
import Monaco from './Monaco'
import { saveAllFiles } from '../state/actions/saveFile'
import { Tab, Tabs } from './Tabs'
import { renameFile } from '../state/actions/createNewFile'
import { Link } from '.'
import Markdown from './Markdown'
const checkWritable = (filename?: string): boolean => {
if (apiHeaderFiles.find(file => file === filename)) {
return false
}
return true
}
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
const filename = editor.getModel()?.uri.path.split('/').pop()
const isWritable = checkWritable(filename)
editor.updateOptions({ readOnly: !isWritable })
}
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 = 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-')
)
// 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 snap = useSnapshot(state);
const { theme } = useTheme();
// useEffect(() => {
// if (snap.editorCtx) {
// snap.editorCtx.getModels().forEach((model) => {
// // console.log(model.id,);
// snap.editorCtx?.createModel(model.getValue(), "c", model.uri);
// });
// }
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, []);
console.log("reinit");
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()
const [isMdPreview, setIsMdPreview] = useState(true)
useEffect(() => {
if (editorRef.current) validateWritability(editorRef.current)
}, [snap.active])
useEffect(() => {
return () => {
subscriptionRef?.current?.close()
}
}, [])
useEffect(() => {
if (monacoRef.current) {
setMarkers(monacoRef.current)
}
}, [snap.active])
useEffect(() => {
return () => {
saveAllFiles()
}
}, [])
const file = snap.files[snap.active]
const renderNav = () => (
<Tabs
label="File"
activeIndex={snap.active}
onChangeActive={idx => (state.active = idx)}
extensionRequired
onCreateNewTab={createNewFile}
onCloseTab={idx => state.files.splice(idx, 1)}
onRenameTab={(idx, nwName, oldName = '') => renameFile(oldName, nwName)}
headerExtraValidation={{
regex: /^[A-Za-z0-9_-]+[.][A-Za-z0-9]{1,4}$/g,
error: 'Filename can contain only characters from a-z, A-Z, 0-9, "_" and "-"'
}}
>
{snap.files.map((file, index) => {
return <Tab key={file.name} header={file.name} renameDisabled={!checkWritable(file.name)} />
})}
</Tabs>
)
const previewToggle = (
<Link
onClick={() => {
if (!isMdPreview) {
saveFile(false)
}
setIsMdPreview(!isMdPreview)
}}
css={{
position: 'absolute',
right: 0,
bottom: 0,
zIndex: 10,
m: '$1',
fontSize: '$sm'
}}
>
{isMdPreview ? 'Exit Preview' : 'View Preview'}
</Link>
)
return (
<Box
css={{
flex: 1,
flexShrink: 1,
display: "flex",
position: "relative",
flexDirection: "column",
backgroundColor: "$slate3",
width: "100%",
display: 'flex',
position: 'relative',
flexDirection: 'column',
backgroundColor: '$mauve2',
width: '100%'
}}
>
<EditorNavigation />
<Editor
keepCurrentModel
// defaultLanguage={snap.files?.[snap.active]?.language}
path={snap.files?.[snap.active]?.name}
// defaultValue={snap.files?.[snap.active]?.content}
beforeMount={(monaco) => {
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;
// hook editor to global state
editor.updateOptions({
minimap: {
enabled: false,
},
...snap.editorSettings,
});
editor.addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S,
() => {
saveFile(editor.getValue());
}
);
}}
theme={theme === "dark" ? "dark" : "light"}
/>
<Button
variant="primary"
uppercase
onClick={() => compileCode(snap.active)}
disabled={snap.compiling}
css={{
position: "absolute",
bottom: "$4",
left: "$4",
alignItems: "center",
display: "flex",
cursor: "pointer",
}}
>
<Play weight="bold" size="16px" />
Compile to Wasm
{snap.compiling && <Spinner />}
</Button>
</Box>
);
};
<EditorNavigation renderNav={renderNav} />
{file?.language === 'markdown' && previewToggle}
{snap.files.length > 0 && router.isReady ? (
isMdPreview && file?.language === 'markdown' ? (
<Markdown
components={{
a: ({ href, children }) => (
<Link target="_blank" rel="noopener noreferrer" href={href}>
{children}
</Link>
)
}}
>
{file.content}
</Markdown>
) : (
<Monaco
keepCurrentModel
defaultLanguage={file?.language}
language={file?.language}
path={`file:///work/c/${file?.name}`}
defaultValue={file?.content}
// onChange={val => (state.files[snap.active].content = val)} // Auto save?
beforeMount={monaco => {
// if (!snap.editorCtx) {
// snap.files.forEach(file =>
// monaco.editor.createModel(
// file.content,
// file.language,
// monaco.Uri.parse(`file:///work/c/${file.name}`)
// )
// )
// }
export default HooksEditor;
// create the web socket
if (!subscriptionRef.current) {
monaco.languages.register({
id: 'c',
extensions: ['.c', '.h'],
aliases: ['C', 'c', 'H', 'h'],
mimetypes: ['text/plain']
})
monaco.languages.register({
id: 'text',
extensions: ['.txt'],
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.dispose()
} catch (err) {
console.log('err', err)
}
})
}
})
}
// editor.updateOptions({
// minimap: {
// enabled: false,
// },
// ...snap.editorSettings,
// });
if (!state.editorCtx) {
state.editorCtx = ref(monaco.editor)
}
}}
onMount={(editor, monaco) => {
editorRef.current = editor
monacoRef.current = monaco
editor.updateOptions({
glyphMargin: true,
lightbulb: {
enabled: true
}
})
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
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)
}
})
// Hacky way to hide Peek menu
editor.onContextMenu(e => {
const host = document.querySelector<HTMLElement>('.shadow-root-host')
const contextMenuItems = host?.shadowRoot?.querySelectorAll('li.action-item')
contextMenuItems?.forEach(k => {
// If menu item contains "Peek" lets hide it
if (k.querySelector('.action-label')?.textContent === 'Peek') {
// @ts-expect-error
k['style'].display = 'none'
}
})
})
validateWritability(editor)
}}
theme={theme === 'dark' ? 'dark' : 'light'}
/>
)
) : (
<Container>
{!snap.loading && router.isReady && (
<Box
css={{
flexDirection: 'row',
width: '$spaces$wide',
gap: '$3',
display: 'inline-flex'
}}
>
<Box css={{ display: 'inline-flex', pl: '35px' }}>
<ArrowBendLeftUp size={30} />
</Box>
<Box css={{ pl: '0px', pt: '15px', flex: 1, display: 'inline-flex' }}>
<Text
css={{
fontSize: '14px',
maxWidth: '220px',
fontFamily: '$monospace'
}}
>
Click the link above to create your file
</Text>
</Box>
</Box>
)}
</Container>
)}
</Box>
)
}
export default HooksEditor

165
components/Input.tsx Normal file
View File

@@ -0,0 +1,165 @@
import React from 'react'
import { styled } from '../stitches.config'
import * as LabelPrim from '@radix-ui/react-label'
export const Input = styled('input', {
// Reset
appearance: 'none',
borderWidth: '0',
boxSizing: 'border-box',
fontFamily: 'inherit',
outline: 'none',
width: '100%',
flex: '1',
backgroundColor: '$mauve4',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '$sm',
px: '$2',
fontSize: '$md',
lineHeight: 1,
color: '$mauve12',
boxShadow: `0 0 0 1px $colors$mauve8`,
height: 35,
WebkitTapHighlightColor: 'rgba(0,0,0,0)',
'&::before': {
boxSizing: 'border-box'
},
'&::after': {
boxSizing: 'border-box'
},
fontVariantNumeric: 'tabular-nums',
'&:-webkit-autofill': {
boxShadow: 'inset 0 0 0 1px $colors$blue6, inset 0 0 0 100px $colors$blue3'
},
'&:-webkit-autofill::first-line': {
fontFamily: '$untitled',
color: '$mauve12'
},
'&:focus': {
boxShadow: `0 0 0 1px $colors$mauve10`,
'&:-webkit-autofill': {
boxShadow: `0 0 0 1px $colors$mauve10`
}
},
'&::placeholder': {
color: '$mauve9'
},
'&:disabled': {
pointerEvents: 'none',
backgroundColor: '$mauve2',
color: '$mauve8',
cursor: 'not-allowed',
'&::placeholder': {
color: '$mauve7'
}
},
'&:read-only': {
backgroundColor: '$mauve2',
color: '$text',
opacity: 0.8,
'&:focus': {
boxShadow: 'inset 0px 0px 0px 1px $colors$mauve7'
}
},
variants: {
size: {
sm: {
height: '$5',
fontSize: '$1',
lineHeight: '$sizes$4',
'&:-webkit-autofill::first-line': {
fontSize: '$1'
}
},
md: {
height: '$8',
fontSize: '$1',
lineHeight: '$sizes$5',
'&:-webkit-autofill::first-line': {
fontSize: '$1'
}
},
lg: {
height: '$12',
fontSize: '$2',
lineHeight: '$sizes$6',
'&:-webkit-autofill::first-line': {
fontSize: '$3'
}
}
},
variant: {
ghost: {
boxShadow: 'none',
backgroundColor: 'transparent',
'@hover': {
'&:hover': {
boxShadow: 'inset 0 0 0 1px $colors$mauve7'
}
},
'&:focus': {
backgroundColor: '$loContrast',
boxShadow: `0 0 0 1px $colors$mauve10`
},
'&:disabled': {
backgroundColor: 'transparent'
},
'&:read-only': {
backgroundColor: 'transparent'
}
},
deep: {
backgroundColor: '$deep',
boxShadow: 'none'
}
},
state: {
invalid: {
boxShadow: 'inset 0 0 0 1px $colors$crimson7',
'&:focus': {
boxShadow: 'inset 0px 0px 0px 1px $colors$crimson8, 0px 0px 0px 1px $colors$crimson8'
}
},
valid: {
boxShadow: 'inset 0 0 0 1px $colors$grass7',
'&:focus': {
boxShadow: 'inset 0px 0px 0px 1px $colors$grass8, 0px 0px 0px 1px $colors$grass8'
}
}
},
cursor: {
default: {
cursor: 'default',
'&:focus': {
cursor: 'text'
}
},
text: {
cursor: 'text'
}
}
},
defaultVariants: {
size: 'md'
}
})
// 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
const LabelRoot = (props: LabelPrim.LabelProps) => <LabelPrim.Root {...props} />
export const Label = styled(LabelRoot, {
display: 'inline-block',
mb: '$1'
})

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

178
components/LogBox.tsx Normal file
View File

@@ -0,0 +1,178 @@
import { useRef, useLayoutEffect, ReactNode, FC, useState } from 'react'
import { IconProps, 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 { ILog } from '../state'
import { Pre, Link, Heading, Button, Text, Flex, Box } from '.'
interface ILogBox {
title: string
clearLog?: () => void
logs: ILog[]
renderNav?: () => ReactNode
enhanced?: boolean
Icon?: FC<IconProps>
}
const LogBox: FC<ILogBox> = ({
title,
clearLog,
logs,
children,
renderNav,
enhanced,
Icon = Notepad
}) => {
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'
}}
>
<Icon size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
</Heading>
<Flex
row
align="center"
// css={{
// maxWidth: "100%", // 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,
timestring,
message,
link,
linkText,
defaultCollapsed,
jsonData
}) => {
const [expanded, setExpanded] = useState(!defaultCollapsed)
if (message === undefined) message = <Text muted>{'undefined'}</Text>
else if (message === '') message = <Text muted>{'""'}</Text>
return (
<>
<LogText variant={type}>
{timestring && (
<Text muted monospace>
{timestring}{' '}
</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>
</>
)
}
export default LogBox

View File

@@ -1,27 +1,31 @@
import { styled } from "../stitches.config";
import { styled } from '../stitches.config'
const Text = styled("span", {
fontFamily: "$monospace",
lineHeight: "$body",
color: "$text",
const Text = styled('span', {
fontFamily: '$monospace',
lineHeight: '$body',
color: '$text',
wordWrap: 'break-word',
variants: {
variant: {
log: {
color: "$text",
color: '$text'
},
warning: {
color: "$yellow11",
color: '$warning'
},
error: {
color: "$red11",
color: '$error'
},
success: {
color: '$success'
}
},
capitalize: {
true: {
textTransform: "capitalize",
},
},
},
});
textTransform: 'capitalize'
}
}
}
})
export default Text;
export default Text

View File

@@ -1,39 +1,28 @@
import { useTheme } from "next-themes";
import { styled } from '../stitches.config'
import { styled } from "../stitches.config";
const SVG = styled("svg", {
"& #path-one, & #path-two": {
fill: "$text",
},
});
function Logo({
width,
height,
}: {
width?: string | number;
height?: string | number;
}) {
const SVG = styled('svg', {
'& #path': {
fill: '$accent'
}
})
function Logo({ width, height }: { width?: string | number; height?: string | number }) {
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>
);
)
}
export default Logo;
export default Logo

14
components/Markdown.tsx Normal file
View File

@@ -0,0 +1,14 @@
import ReactMarkdown from 'react-markdown'
import { styled } from '../stitches.config'
const Markdown = styled(ReactMarkdown, {
px: '$8',
'@md': {
px: '$20'
},
pb: '$5',
height: '100%',
overflowY: 'auto'
})
export default Markdown

71
components/Monaco.tsx Normal file
View File

@@ -0,0 +1,71 @@
import Editor, { loader, EditorProps, Monaco } from '@monaco-editor/react'
import { CSS } from '@stitches/react'
import type monaco from 'monaco-editor'
import { useTheme } from 'next-themes'
import { FC, MutableRefObject, ReactNode } from 'react'
import { Flex } from '.'
import dark from '../theme/editor/amy.json'
import light from '../theme/editor/xcode_default.json'
export type MonacoProps = EditorProps & {
id?: string
rootProps?: { css: CSS } & Record<string, any>
overlay?: ReactNode
editorRef?: MutableRefObject<monaco.editor.IStandaloneCodeEditor>
monacoRef?: MutableRefObject<typeof monaco>
}
loader.config({
paths: {
vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs'
}
})
const Monaco: FC<MonacoProps> = ({
id,
path = `file:///${id}`,
className = id,
language = 'json',
overlay,
editorRef,
monacoRef,
beforeMount,
rootProps,
...rest
}) => {
const { theme } = useTheme()
const setTheme = (monaco: Monaco) => {
monaco.editor.defineTheme('dark', dark as any)
monaco.editor.defineTheme('light', light as any)
}
return (
<Flex
fluid
column
{...rootProps}
css={{
position: 'relative',
height: '100%',
...rootProps?.css
}}
>
<Editor
className={className}
language={language}
path={path}
beforeMount={monaco => {
beforeMount?.(monaco)
setTheme(monaco)
}}
theme={theme === 'dark' ? 'dark' : 'light'}
{...rest}
/>
{overlay && (
<Flex css={{ position: 'absolute', bottom: 0, right: 0, width: '100%' }}>{overlay}</Flex>
)}
</Flex>
)
}
export default Monaco

View File

@@ -1,160 +1,369 @@
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 React from 'react'
import Link from 'next/link'
import Stack from "./Stack";
import Logo from "./Logo";
import Button from "./Button";
import Container from "./Container";
import Box from "./Box";
import Flex from "./Flex";
import ThemeChanger from "./ThemeChanger";
import { useRouter } from "next/router";
import { useSnapshot } from 'valtio'
import { useRouter } from 'next/router'
import { FolderOpen, X, ArrowUpRight, BookOpen } from 'phosphor-react'
import Stack from './Stack'
import Logo from './Logo'
import Button from './Button'
import Flex from './Flex'
import Container from './Container'
import Box from './Box'
import ThemeChanger from './ThemeChanger'
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'
import { styled } from '../stitches.config'
const ImageWrapper = styled(Flex, {
position: 'relative',
mt: '$2',
mb: '$10',
svg: {
// fill: "red",
'.angle': {
fill: '$text'
},
':not(.angle)': {
stroke: '$text'
}
}
})
const Navigation = () => {
const { data: session, status } = useSession();
const router = useRouter();
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",
borderBottom: "1px solid $slate6",
position: "relative",
display: 'flex',
backgroundColor: '$mauve1',
borderBottom: '1px solid $mauve6',
position: 'relative',
zIndex: 2003,
height: '60px'
}}
>
<Container
css={{
display: "flex",
alignItems: "center",
py: "$2",
display: 'flex',
alignItems: 'center'
}}
>
<Link href="/" passHref>
<Box
as="a"
css={{ display: "flex", alignItems: "center", color: "$textColor" }}
>
<Logo width="30px" height="30px" />
</Box>
</Link>
<Stack css={{ ml: "$4", gap: "$3" }}>
<Link href="/develop" passHref shallow>
<Button
<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"
outline={!router.pathname.includes("/develop")}
uppercase
>
Develop
</Button>
</Link>
<Link href="/deploy" passHref shallow>
<Button
as="a"
outline={!router.pathname.includes("/deploy")}
uppercase
>
Deploy
</Button>
</Link>
<Link href="/test" passHref shallow>
<Button
as="a"
outline={!router.pathname.includes("/test")}
uppercase
>
Test
</Button>
</Link>
</Stack>
<Stack css={{ color: "text", ml: "auto" }}>
<ThemeChanger />
{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 === "unauthenticated" && (
<Button outline onClick={() => signIn("github")}>
<GithubLogo size="16px" /> Github Login
</Button>
)}
{status === "loading" && "loading"}
{/* <Box
css={{
border: "1px solid",
borderRadius: "3px",
borderColor: "text",
p: 1,
display: 'flex',
alignItems: 'center',
color: '$textColor'
}}
>
<BookOpen size="20px" />
</Box> */}
</Stack>
<Logo width="32px" height="32px" />
</Box>
</Link>
<Flex
css={{
ml: '$5',
flexDirection: 'column',
gap: '1px'
}}
>
{snap.loading ? (
<Spinner />
) : (
<>
<Heading css={{ lineHeight: 1 }}>{snap.gistName || 'XRPL Hooks'}</Heading>
<Text css={{ fontSize: '$xs', color: '$mauve10', lineHeight: 1 }}>
{snap.files.length > 0 ? 'Gist: ' : 'Builder'}
{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={{
display: 'flex',
maxWidth: '1080px',
width: '80vw',
maxHeight: '80%',
backgroundColor: '$mauve1 !important',
overflowY: 'auto',
background: 'black',
p: 0
}}
>
<Flex
css={{
flexDirection: 'column',
height: '100%',
'@md': {
flexDirection: 'row',
height: '100%'
}
}}
>
<Flex
css={{
borderBottom: '1px solid $colors$mauve5',
width: '100%',
minWidth: '240px',
flexDirection: 'column',
p: '$7',
backgroundColor: '$mauve2',
'@md': {
width: '30%',
maxWidth: '300px',
borderBottom: '0px',
borderRight: '1px solid $colors$mauve5'
}
}}
>
<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: '$purple11',
'&:hover': {
color: '$purple12'
},
'&: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: '$purple11',
'&:hover': {
color: '$purple12'
},
'&: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: '$purple11',
'&:hover': {
color: '$purple12'
},
'&: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>
<Flex
css={{
display: 'grid',
gridTemplateColumns: '1fr',
gridTemplateRows: 'max-content',
flex: 1,
p: '$7',
pb: '$16',
gap: '$3',
alignItems: 'normal',
flexWrap: 'wrap',
backgroundColor: '$mauve1',
'@md': {
gridTemplateColumns: '1fr 1fr',
gridTemplateRows: 'max-content'
},
'@lg': {
gridTemplateColumns: '1fr 1fr 1fr',
gridTemplateRows: 'max-content'
}
}}
>
{Object.values(templateFileIds).map(template => (
<PanelBox key={template.id} as="a" href={`/develop/${template.id}`}>
<ImageWrapper>{template.icon()}</ImageWrapper>
<Heading>{template.name}</Heading>
<Text>{template.description}</Text>
</PanelBox>
))}
</Flex>
</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: '$4',
overflowX: 'scroll',
'&::-webkit-scrollbar': {
height: 0,
background: 'transparent'
},
scrollbarColor: 'transparent',
scrollbarWidth: 'none'
}}
>
<Stack
css={{
ml: '$4',
gap: '$3',
flexWrap: 'nowrap',
alignItems: 'center',
marginLeft: 'auto'
}}
>
<ButtonGroup>
<Link href={gistId ? `/develop/${gistId}` : '/develop'} passHref shallow>
<Button as="a" outline={!router.pathname.includes('/develop')} uppercase>
Develop
</Button>
</Link>
<Link href={gistId ? `/deploy/${gistId}` : '/deploy'} passHref shallow>
<Button as="a" outline={!router.pathname.includes('/deploy')} uppercase>
Deploy
</Button>
</Link>
<Link href={gistId ? `/test/${gistId}` : '/test'} passHref shallow>
<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>
</Stack>
</Flex>
</Container>
</Box>
);
};
)
}
export default Navigation;
export default Navigation

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

102
components/Popover.tsx Normal file
View File

@@ -0,0 +1,102 @@
import React, { ReactNode } from 'react'
import * as PopoverPrimitive from '@radix-ui/react-popover'
import { styled, keyframes } from '../stitches.config'
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(PopoverPrimitive.Content, {
borderRadius: 4,
padding: '$3 $3',
fontSize: 12,
lineHeight: 1,
color: '$text',
backgroundColor: '$background',
boxShadow:
'0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)',
'@media (prefers-reduced-motion: no-preference)': {
animationDuration: '400ms',
animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
willChange: 'transform, opacity',
'&[data-state="open"]': {
'&[data-side="top"]': { animationName: slideDownAndFade },
'&[data-side="right"]': { animationName: slideLeftAndFade },
'&[data-side="bottom"]': { animationName: slideUpAndFade },
'&[data-side="left"]': { animationName: slideRightAndFade }
}
},
'.dark &': {
backgroundColor: '$mauve5',
boxShadow: '0px 5px 38px -2px rgba(22, 23, 24, 1), 0px 10px 20px 0px rgba(22, 23, 24, 1)'
}
})
const StyledArrow = styled(PopoverPrimitive.Arrow, {
fill: '$colors$mauve2',
'.dark &': {
fill: '$mauve5'
}
})
const StyledClose = styled(PopoverPrimitive.Close, {
all: 'unset',
fontFamily: 'inherit',
borderRadius: '100%',
height: 25,
width: 25,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
color: '$text',
position: 'absolute',
top: 5,
right: 5
})
// Exports
export const PopoverRoot = PopoverPrimitive.Root
export const PopoverTrigger = PopoverPrimitive.Trigger
export const PopoverContent = StyledContent
export const PopoverArrow = StyledArrow
export const PopoverClose = StyledClose
interface IPopover {
content: string | ReactNode
open?: boolean
defaultOpen?: boolean
onOpenChange?: (open: boolean) => void
}
const Popover: React.FC<IPopover & React.ComponentProps<typeof PopoverContent>> = ({
children,
content,
open,
defaultOpen = false,
onOpenChange,
...rest
}) => (
<PopoverRoot open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
<PopoverTrigger asChild>{children}</PopoverTrigger>
<PopoverContent sideOffset={5} {...rest}>
{content} <PopoverArrow offset={5} className="arrow" />
</PopoverContent>
</PopoverRoot>
)
export default Popover

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

24
components/ResultLink.tsx Normal file
View File

@@ -0,0 +1,24 @@
import { FC } from 'react'
import { Link } from '.'
interface Props {
result?: string
}
const ResultLink: FC<Props> = ({ result }) => {
if (!result) return null
let href: string
if (result === 'tesSUCCESS') {
href = 'https://xrpl.org/tes-success.html'
} else {
// Going shortcut here because of url structure, if that changes we will do it manually
href = `https://xrpl.org/${result.slice(0, 3)}-codes.html`
}
return (
<Link as="a" href={href} target="_blank" rel="noopener noreferrer">
{result}
</Link>
)
}
export default ResultLink

View File

@@ -0,0 +1,333 @@
import { Play, X } from 'phosphor-react'
import { HTMLInputTypeAttribute, useCallback, useEffect, useState } from 'react'
import state, { IAccount, IFile, ILog } from '../../state'
import Button from '../Button'
import Box from '../Box'
import Input, { Label } from '../Input'
import Stack from '../Stack'
import {
Dialog,
DialogTrigger,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose
} from '../Dialog'
import Flex from '../Flex'
import { useSnapshot } from 'valtio'
import Select from '../Select'
import Text from '../Text'
import { saveFile } from '../../state/actions/saveFile'
import { getErrors, getTags } from '../../utils/comment-parser'
import toast from 'react-hot-toast'
const generateHtmlTemplate = async (code: string, data?: Record<string, any>) => {
let processString: string | undefined
const process = { env: { NODE_ENV: 'production' } } as any
if (data) {
Object.keys(data).forEach(key => {
process.env[key] = data[key]
})
}
processString = JSON.stringify(process)
const libs = (await import("xrpl-accountlib/dist/browser.hook-bundle.js")).default;
return `
<html>
<head>
<script>
var log = console.log;
var errorLog = console.error;
var infoLog = console.info;
var warnLog = console.warn
console.log = function(){
var args = Array.from(arguments);
parent.window.postMessage({ type: 'log', args: args || [] }, '*');
log.apply(console, args);
}
console.error = function(){
var args = Array.from(arguments);
parent.window.postMessage({ type: 'error', args: args || [] }, '*');
errorLog.apply(console, args);
}
console.info = function(){
var args = Array.from(arguments);
parent.window.postMessage({ type: 'info', args: args || [] }, '*');
infoLog.apply(console, args);
}
console.warn = function(){
var args = Array.from(arguments);
parent.window.postMessage({ type: 'warning', args: args || [] }, '*');
warnLog.apply(console, args);
}
var process = '${processString || '{}'}';
process = JSON.parse(process);
window.process = process
function windowErrorHandler(event) {
event.preventDefault() // to prevent automatically logging to console
console.error(event.error?.toString())
}
window.addEventListener('error', windowErrorHandler);
</script>
<script>
${libs}
</script>
<script type="module">
${code}
</script>
</head>
<body>
</body>
</html>
`
}
type Fields = Record<
string,
{
name: string
value: string
type?: 'Account' | `Account.${keyof IAccount}` | HTMLInputTypeAttribute
description?: string
required?: boolean
}
>
const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
const snap = useSnapshot(state)
const [templateError, setTemplateError] = useState('')
const [fields, setFields] = useState<Fields>({})
const [iFrameCode, setIframeCode] = useState('')
const [isDialogOpen, setIsDialogOpen] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const getFields = useCallback(() => {
const inputTags = ['input', 'param', 'arg', 'argument']
const tags = getTags(content)
.filter(tag => inputTags.includes(tag.tag))
.filter(tag => !!tag.name)
let _fields = tags.map(tag => ({
name: tag.name,
value: tag.default || '',
type: tag.type,
description: tag.description,
required: !tag.optional
}))
const fields: Fields = _fields.reduce((acc, field) => {
acc[field.name] = field
return acc
}, {} as Fields)
const error = getErrors(content)
if (error) setTemplateError(error.message)
else setTemplateError('')
return fields
}, [content])
const runScript = useCallback(async () => {
setIsLoading(true);
try {
// Show loading toast only after 1 second, otherwise skip it.
let loaded = false
let toastId: string | undefined;
setTimeout(() => {
if (!loaded) {
toastId = toast.loading('Loading packages, may take a few seconds...', {
position: 'bottom-center',
})
}
}, 1000)
let data: any = {}
Object.keys(fields).forEach(key => {
data[key] = fields[key].value
})
const template = await generateHtmlTemplate(content, data)
setIframeCode(template)
loaded = true
if (toastId) {
toast.dismiss(toastId)
}
state.scriptLogs = [{ type: 'success', message: 'Started running...' }]
} catch (err) {
state.scriptLogs = [
...snap.scriptLogs,
// @ts-expect-error
{ type: 'error', message: err?.message || 'Could not parse template' }
]
}
setIsLoading(false);
}, [content, fields, snap.scriptLogs])
useEffect(() => {
const handleEvent = (e: any) => {
if (e.data.type === 'log' || e.data.type === 'error') {
const data: ILog[] = e.data.args.map((msg: any) => ({
type: e.data.type,
message: typeof msg === 'string' ? msg : JSON.stringify(msg, null, 2)
}))
state.scriptLogs = [...snap.scriptLogs, ...data]
}
}
window.addEventListener('message', handleEvent)
return () => window.removeEventListener('message', handleEvent)
}, [snap.scriptLogs])
useEffect(() => {
const defaultFields = getFields() || {}
setFields(defaultFields)
}, [content, setFields, getFields])
const accOptions = snap.accounts?.map(acc => ({
...acc,
label: acc.name,
value: acc.address
}))
const isDisabled = Object.values(fields).some(field => field.required && !field.value)
const handleRun = useCallback(async () => {
if (isDisabled) return toast.error('Please fill in all the required fields.')
state.scriptLogs = []
await runScript();
setIsDialogOpen(false)
}, [isDisabled, runScript])
return (
<>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button
variant="primary"
onClick={() => {
saveFile(false)
setIframeCode('')
}}
>
<Play weight="bold" size="16px" /> {name}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Run {name} script</DialogTitle>
<DialogDescription>
<Box>
You are about to run scripts provided by the developer of the hook, make sure you
trust the author before you continue.
</Box>
{templateError && (
<Box
as="span"
css={{
display: 'block',
color: '$error',
mt: '$3',
whiteSpace: 'pre'
}}
>
{templateError}
</Box>
)}
{Object.keys(fields).length > 0 && (
<Box css={{ mt: '$4', mb: 0 }}>
Fill in the following parameters to run the script.
</Box>
)}
</DialogDescription>
<Stack css={{ width: '100%' }}>
{Object.keys(fields).map(key => {
const { name, value, type, description, required } = fields[key]
const isAccount = type?.startsWith('Account')
const isAccountSecret = type === 'Account.secret'
const accountField = (isAccount && type?.split('.')[1]) || 'address'
return (
<Box key={name} css={{ width: '100%' }}>
<Label css={{ display: 'flex', justifyContent: 'space-between' }}>
<span>
{description || name} {required && <Text error>*</Text>}
</span>
{isAccountSecret && (
<Text error small css={{ alignSelf: 'end' }}>
can access account secret key
</Text>
)}
</Label>
{isAccount ? (
<Select
css={{ mt: '$1' }}
options={accOptions}
onChange={(val: any) => {
setFields({
...fields,
[key]: {
...fields[key],
value: val[accountField]
}
})
}}
value={accOptions.find((acc: any) => acc[accountField] === value)}
/>
) : (
<Input
type={type || 'text'}
value={value}
css={{ mt: '$1' }}
onChange={e => {
setFields({
...fields,
[key]: { ...fields[key], value: e.target.value }
})
}}
/>
)}
</Box>
)
})}
<Flex css={{ justifyContent: 'flex-end', width: '100%', gap: '$3' }}>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<Button variant="primary" isDisabled={isDisabled || isLoading} isLoading={isLoading} onClick={handleRun}>
Run script
</Button>
</Flex>
</Stack>
<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>
{iFrameCode && (
<iframe style={{ display: 'none' }} srcDoc={iFrameCode} sandbox="allow-scripts" />
)}
</>
)
}
export default RunScript

123
components/Select.tsx Normal file
View File

@@ -0,0 +1,123 @@
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',
width: '100%'
}
},
singleValue: provided => ({
...provided,
color: colors.mauve12
}),
menu: provided => ({
...provided,
backgroundColor: colors.dropDownBg
}),
control: (provided, state) => {
return {
...provided,
minHeight: 0,
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.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
}
}
}
}}
{...props}
/>
)
})
export default styled(Select, {})

71
components/Sequence.tsx Normal file
View File

@@ -0,0 +1,71 @@
import { FC, useCallback, useState } from 'react'
import state from '../state'
import { Flex, Input, Button } from '.'
import fetchAccountInfo from '../utils/accountInfo'
import { useSnapshot } from 'valtio'
interface AccountSequenceProps {
address?: string
}
const AccountSequence: FC<AccountSequenceProps> = ({ address }) => {
const { accounts } = useSnapshot(state)
const account = accounts.find(acc => acc.address === address)
const [isLoading, setIsLoading] = useState(false)
const setSequence = useCallback(
(sequence: number) => {
const acc = state.accounts.find(acc => acc.address == address)
if (!acc) return
acc.sequence = sequence
},
[address]
)
const handleUpdateSequence = useCallback(
async (silent?: boolean) => {
if (!account) return
setIsLoading(true)
const info = await fetchAccountInfo(account.address, { silent })
if (info) {
setSequence(info.Sequence)
}
setIsLoading(false)
},
[account, setSequence]
)
const disabled = !account
return (
<Flex row align="center" fluid>
<Input
placeholder="Account sequence"
value={account?.sequence}
disabled={!account}
type="number"
readOnly={true}
/>
<Button
size="xs"
variant="primary"
type="button"
outline
disabled={disabled}
isDisabled={disabled}
isLoading={isLoading}
css={{
background: '$backgroundAlt',
position: 'absolute',
right: '$2',
fontSize: '$xs',
cursor: 'pointer',
alignContent: 'center',
display: 'flex'
}}
onClick={() => handleUpdateSequence()}
>
Update
</Button>
</Flex>
)
}
export default AccountSequence

View File

@@ -0,0 +1,425 @@
import React, { useCallback, useEffect, useState } from 'react'
import { Plus, Trash, X } from 'phosphor-react'
import { Button, Box, Text } from '.'
import { Stack, Flex, Select } from '.'
import {
Dialog,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose,
DialogTrigger
} from './Dialog'
import { Input, Label } from './Input'
import { Controller, SubmitHandler, useFieldArray, useForm } from 'react-hook-form'
import { deployHook } from '../state/actions'
import { useSnapshot } from 'valtio'
import state, { IFile, SelectOption } from '../state'
import toast from 'react-hot-toast'
import { prepareDeployHookTx, sha256 } from '../state/actions/deployHook'
import estimateFee from '../utils/estimateFee'
import { getParameters, getInvokeOptions, transactionOptions, SetHookData } from '../utils/setHook'
import { capitalize } from '../utils/helpers'
import AccountSequence from './Sequence'
export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
({ accountAddress }) => {
const snap = useSnapshot(state)
const [estimateLoading, setEstimateLoading] = useState(false)
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false)
const compiledFiles = snap.files.filter(file => file.compiledContent)
const activeFile = compiledFiles[snap.activeWat] as IFile | undefined
const accountOptions: SelectOption[] = snap.accounts.map(acc => ({
label: acc.name,
value: acc.address
}))
const [selectedAccount, setSelectedAccount] = useState(
accountOptions.find(acc => acc.value === accountAddress)
)
const account = snap.accounts.find(acc => acc.address === selectedAccount?.value)
const getHookNamespace = useCallback(
() =>
(activeFile && snap.deployValues[activeFile.name]?.HookNamespace) ||
activeFile?.name.split('.')[0] ||
'',
[activeFile, snap.deployValues]
)
const getDefaultValues = useCallback((): Partial<SetHookData> => {
const content = activeFile?.compiledValueSnapshot
return (
(activeFile && snap.deployValues[activeFile.name]) || {
HookNamespace: getHookNamespace(),
Invoke: getInvokeOptions(content),
HookParameters: getParameters(content)
}
)
}, [activeFile, getHookNamespace, snap.deployValues])
const {
register,
handleSubmit,
control,
watch,
setValue,
getValues,
reset,
formState: { errors }
} = useForm<SetHookData>({
defaultValues: getDefaultValues()
})
const { fields, append, remove } = useFieldArray({
control,
name: 'HookParameters' // unique name for your Field Array
})
const watchedFee = watch('Fee')
// Reset form if activeFile changes
useEffect(() => {
if (!activeFile) return
const defaultValues = getDefaultValues()
reset(defaultValues)
}, [activeFile, getDefaultValues, reset])
useEffect(() => {
if (watchedFee && (watchedFee.includes('.') || watchedFee.includes(','))) {
setValue('Fee', watchedFee.replaceAll('.', '').replaceAll(',', ''))
}
}, [watchedFee, setValue])
// const {
// fields: grantFields,
// append: grantAppend,
// remove: grantRemove,
// } = useFieldArray({
// control,
// name: "HookGrants", // unique name for your Field Array
// });
const [hashedNamespace, setHashedNamespace] = useState('')
const namespace = watch('HookNamespace', getHookNamespace())
const calculateHashedValue = useCallback(async () => {
const hashedVal = await sha256(namespace)
setHashedNamespace(hashedVal.toUpperCase())
}, [namespace])
useEffect(() => {
calculateHashedValue()
}, [namespace, calculateHashedValue])
const calculateFee = useCallback(async () => {
if (!account) return
const formValues = getValues()
const tx = await prepareDeployHookTx(account, formValues)
if (!tx) {
return
}
const res = await estimateFee(tx, account)
if (res && res.base_fee) {
setValue('Fee', Math.round(Number(res.base_fee || '')).toString())
}
}, [account, getValues, setValue])
const tooLargeFile = () => {
return Boolean(
activeFile?.compiledContent?.byteLength && activeFile?.compiledContent?.byteLength >= 64000
)
}
const onSubmit: SubmitHandler<SetHookData> = async data => {
const currAccount = state.accounts.find(acc => acc.address === account?.address)
if (!account) return
if (currAccount) currAccount.isLoading = true
data.HookParameters.forEach(param => {
delete param.$metaData
return param
})
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})`)
}
const onOpenChange = useCallback(
(open: boolean) => {
setIsSetHookDialogOpen(open)
if (open) calculateFee()
},
[calculateFee]
)
return (
<Dialog open={isSetHookDialogOpen} onOpenChange={onOpenChange}>
<DialogTrigger asChild>
<Button
ghost
size="xs"
uppercase
variant={'secondary'}
disabled={!account || account.isLoading || !activeFile || tooLargeFile()}
>
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>Account</Label>
<Select
instanceId="deploy-account"
placeholder="Select account"
options={accountOptions}
value={selectedAccount}
onChange={(acc: any) => setSelectedAccount(acc)}
/>
</Box>
<Box css={{ width: '100%', position: 'relative' }}>
<Label>Sequence</Label>
<AccountSequence address={selectedAccount?.value} />
</Box>
<Box css={{ width: '100%' }}>
<Label>Invoke on transactions</Label>
<Controller
name="Invoke"
control={control}
render={({ field }) => (
<Select
{...field}
closeMenuOnSelect={false}
isMulti
menuPosition="fixed"
options={transactionOptions}
/>
)}
/>
</Box>
<Box css={{ width: '100%' }}>
<Label>Hook Namespace Seed</Label>
<Input {...register('HookNamespace', { required: true })} autoComplete={'off'} />
{errors.HookNamespace?.type === 'required' && (
<Box css={{ display: 'inline', color: '$red11' }}>Namespace is required</Box>
)}
<Box css={{ mt: '$3' }}>
<Label>Hook Namespace (sha256)</Label>
<Input readOnly value={hashedNamespace} />
</Box>
</Box>
<Box css={{ width: '100%' }}>
<Label style={{ marginBottom: '10px', display: 'block' }}>Hook parameters</Label>
<Stack>
{fields.map((field, index) => (
<Stack key={field.id}>
<Flex column>
<Flex row>
<Input
// important to include key with field's id
placeholder="Parameter name"
readOnly={field.$metaData?.required}
{...register(
`HookParameters.${index}.HookParameter.HookParameterName`
)}
/>
<Input
css={{ mx: '$2' }}
placeholder="Value (hex-quoted)"
{...register(
`HookParameters.${index}.HookParameter.HookParameterValue`,
{ required: field.$metaData?.required }
)}
/>
<Button onClick={() => remove(index)} variant="destroy">
<Trash weight="regular" size="16px" />
</Button>
</Flex>
{errors.HookParameters?.[index]?.HookParameter?.HookParameterValue
?.type === 'required' && <Text error>This field is required</Text>}
<Label css={{ fontSize: '$sm', mt: '$1' }}>
{capitalize(field.$metaData?.description)}
</Label>
</Flex>
</Stack>
))}
<Button
outline
fullWidth
type="button"
onClick={() =>
append({
HookParameter: {
HookParameterName: '',
HookParameterValue: ''
}
})
}
>
<Plus size="16px" />
Add Hook Parameter
</Button>
</Stack>
</Box>
<Box css={{ width: '100%', position: 'relative' }}>
<Label>Fee</Label>
<Box css={{ display: 'flex', alignItems: 'center' }}>
<Input
type="number"
{...register('Fee', { required: true })}
autoComplete={'off'}
onKeyPress={e => {
if (e.key === '.' || e.key === ',') {
e.preventDefault()
}
}}
step="1"
defaultValue={10000}
css={{
'-moz-appearance': 'textfield',
'&::-webkit-outer-spin-button': {
'-webkit-appearance': 'none',
margin: 0
},
'&::-webkit-inner-spin-button ': {
'-webkit-appearance': 'none',
margin: 0
}
}}
/>
<Button
size="xs"
variant="primary"
outline
isLoading={estimateLoading}
css={{
position: 'absolute',
right: '$2',
fontSize: '$xs',
cursor: 'pointer',
alignContent: 'center',
display: 'flex'
}}
onClick={async e => {
e.preventDefault()
if (!account) return
setEstimateLoading(true)
const formValues = getValues()
try {
const tx = await prepareDeployHookTx(account, formValues)
if (tx) {
const res = await estimateFee(tx, account)
if (res && res.base_fee) {
setValue('Fee', Math.round(Number(res.base_fee || '')).toString())
}
}
} catch (err) {}
setEstimateLoading(false)
}}
>
Suggest
</Button>
</Box>
{errors.Fee?.type === 'required' && (
<Box css={{ display: 'inline', color: '$red11' }}>Fee is required</Box>
)}
</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>
)
}
)
SetHookDialog.displayName = 'SetHookDialog'
export default SetHookDialog

View File

@@ -1,13 +1,14 @@
import { Spinner as SpinnerIcon } from "phosphor-react";
import { styled, keyframes } from "../stitches.config";
import { Spinner as SpinnerIcon } from 'phosphor-react'
import { styled, keyframes } from '../stitches.config'
const rotate = keyframes({
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" },
});
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(-360deg)' }
})
const Spinner = styled(SpinnerIcon, {
animation: `${rotate} 150ms cubic-bezier(0.16, 1, 0.3, 1) infinite`,
});
animation: `${rotate} 150ms linear infinite`,
fontSize: '16px'
})
export default Spinner;
export default Spinner

View File

@@ -1,14 +1,11 @@
import { Children } from "react";
import Box from "./Box";
import { styled } from "../stitches.config";
import type * as Stitches from "@stitches/react";
import Box from './Box'
import { styled } from '../stitches.config'
const StackComponent = styled(Box, {
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
gap: "$4",
});
display: 'flex',
flexWrap: 'wrap',
flexDirection: 'row',
gap: '$4'
})
export default StackComponent;
export default StackComponent

32
components/Switch.tsx Normal file
View File

@@ -0,0 +1,32 @@
import { styled } from '../stitches.config'
import * as SwitchPrimitive from '@radix-ui/react-switch'
const StyledSwitch = styled(SwitchPrimitive.Root, {
all: 'unset',
width: 42,
height: 25,
backgroundColor: '$mauve9',
borderRadius: '9999px',
position: 'relative',
boxShadow: `0 2px 10px $colors$mauve2`,
WebkitTapHighlightColor: 'rgba(0, 0, 0, 0)',
'&:focus': { boxShadow: `0 0 0 2px $colors$mauveA2` },
'&[data-state="checked"]': { backgroundColor: '$green11' }
})
const StyledThumb = styled(SwitchPrimitive.Thumb, {
display: 'block',
width: 21,
height: 21,
backgroundColor: 'white',
borderRadius: '9999px',
boxShadow: `0 2px 2px $colors$mauveA6`,
transition: 'transform 100ms',
transform: 'translateX(2px)',
willChange: 'transform',
'&[data-state="checked"]': { transform: 'translateX(19px)' }
})
// Exports
export const Switch = StyledSwitch
export const SwitchThumb = StyledThumb

374
components/Tabs.tsx Normal file
View File

@@ -0,0 +1,374 @@
import React, { useEffect, useState, Fragment, isValidElement, useCallback } from 'react'
import type { ReactNode, ReactElement } from 'react'
import { Box, Button, Flex, Input, Label, Pre, Stack, Text } from '.'
import {
Dialog,
DialogTrigger,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose
} from './Dialog'
import { Plus, X } from 'phosphor-react'
import { styled } from '../stitches.config'
import { capitalize, getFileExtention } from '../utils/helpers'
import ContextMenu, { ContentMenuOption } from './ContextMenu'
const ErrorText = styled(Text, {
color: '$error',
mt: '$1',
display: 'block'
})
type Nullable<T> = T | null | undefined | false
interface TabProps {
header: string
children?: ReactNode
renameDisabled?: boolean
}
// TODO customize messages shown
interface Props {
label?: string
activeIndex?: number
activeHeader?: string
headless?: boolean
children: ReactElement<TabProps>[]
keepAllAlive?: boolean
defaultExtension?: string
extensionRequired?: boolean
allowedExtensions?: string[]
headerExtraValidation?: {
regex: string | RegExp
error: string
}
onCreateNewTab?: (name: string) => any
onRenameTab?: (index: number, nwName: string, oldName?: string) => any
onCloseTab?: (index: number, header?: string) => any
onChangeActive?: (index: number, header?: string) => any
}
export const Tab = (props: TabProps) => null
export const Tabs = ({
label = 'Tab',
children,
activeIndex,
activeHeader,
headless,
keepAllAlive = false,
onCreateNewTab,
onCloseTab,
onChangeActive,
onRenameTab,
headerExtraValidation,
extensionRequired,
defaultExtension = '',
allowedExtensions
}: Props) => {
const [active, setActive] = useState(activeIndex || 0)
const tabs: TabProps[] = children.map(elem => elem.props)
const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false)
const [renamingTab, setRenamingTab] = useState<number | null>(null)
const [tabname, setTabname] = useState('')
const [tabnameError, setTabnameError] = useState<string | null>(null)
useEffect(() => {
if (activeIndex) setActive(activeIndex)
}, [activeIndex])
useEffect(() => {
if (activeHeader) {
const idx = tabs.findIndex(tab => tab.header === activeHeader)
if (idx !== -1) setActive(idx)
else setActive(0)
}
}, [activeHeader, tabs])
// when filename changes, reset error
useEffect(() => {
setTabnameError(null)
}, [tabname, setTabnameError])
const validateTabname = useCallback(
(tabname: string): { error?: string; result?: string } => {
if (!tabname) {
return { error: `Please enter ${label.toLocaleLowerCase()} name.` }
}
let ext = getFileExtention(tabname)
if (!ext && defaultExtension) {
ext = defaultExtension
tabname = `${tabname}.${defaultExtension}`
}
if (tabs.find(tab => tab.header === tabname)) {
return { error: `${capitalize(label)} name already exists.` }
}
if (extensionRequired && !ext) {
return { error: 'File extension is required!' }
}
if (allowedExtensions && ext && !allowedExtensions.includes(ext)) {
return { error: 'This file extension is not allowed!' }
}
if (headerExtraValidation && !tabname.match(headerExtraValidation.regex)) {
return { error: headerExtraValidation.error }
}
return { result: tabname }
},
[allowedExtensions, defaultExtension, extensionRequired, headerExtraValidation, label, tabs]
)
const handleActiveChange = useCallback(
(idx: number, header?: string) => {
setActive(idx)
onChangeActive?.(idx, header)
},
[onChangeActive]
)
const handleRenameTab = useCallback(() => {
if (renamingTab === null) return
const res = validateTabname(tabname)
if (res.error) {
setTabnameError(`Error: ${res.error}`)
return
}
const { result: nwName = tabname } = res
setRenamingTab(null)
setTabname('')
const oldName = tabs[renamingTab]?.header
onRenameTab?.(renamingTab, nwName, oldName)
handleActiveChange(renamingTab, nwName)
}, [handleActiveChange, onRenameTab, renamingTab, tabname, tabs, validateTabname])
const handleCreateTab = useCallback(() => {
const res = validateTabname(tabname)
if (res.error) {
setTabnameError(`Error: ${res.error}`)
return
}
const { result: _tabname = tabname } = res
setIsNewtabDialogOpen(false)
setTabname('')
onCreateNewTab?.(_tabname)
handleActiveChange(tabs.length, _tabname)
}, [validateTabname, tabname, onCreateNewTab, handleActiveChange, tabs.length])
const handleCloseTab = useCallback(
(idx: number) => {
onCloseTab?.(idx, tabs[idx].header)
if (idx <= active && active !== 0) {
const nwActive = active - 1
handleActiveChange(nwActive, tabs[nwActive].header)
}
},
[active, handleActiveChange, onCloseTab, tabs]
)
const closeOption = (idx: number): Nullable<ContentMenuOption> =>
onCloseTab && {
type: 'text',
label: 'Close',
key: 'close',
onSelect: () => handleCloseTab(idx)
}
const renameOption = (idx: number, tab: TabProps): Nullable<ContentMenuOption> => {
return (
onRenameTab &&
!tab.renameDisabled && {
type: 'text',
label: 'Rename',
key: 'rename',
onSelect: () => setRenamingTab(idx)
}
)
}
return (
<>
{!headless && (
<Stack
css={{
gap: '$3',
flex: 1,
flexWrap: 'nowrap',
marginBottom: '$2',
width: '100%',
overflow: 'auto'
}}
>
{tabs.map((tab, idx) => (
<ContextMenu
key={tab.header}
options={
[closeOption(idx), renameOption(idx, tab)].filter(Boolean) as ContentMenuOption[]
}
>
<Button
role="tab"
tabIndex={idx}
onClick={() => handleActiveChange(idx, tab.header)}
onKeyPress={() => handleActiveChange(idx, tab.header)}
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>
</ContextMenu>
))}
{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 ${label.toLocaleLowerCase()}`}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Create new {label.toLocaleLowerCase()}</DialogTitle>
<DialogDescription>
<Label>{label} name</Label>
<Input
value={tabname}
onChange={e => setTabname(e.target.value)}
onKeyPress={e => {
if (e.key === 'Enter') {
handleCreateTab()
}
}}
/>
<ErrorText>{tabnameError}</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>
)}
{onRenameTab && (
<Dialog open={renamingTab !== null} onOpenChange={() => setRenamingTab(null)}>
<DialogContent>
<DialogTitle>
Rename <Pre>{tabs[renamingTab || 0]?.header}</Pre>
</DialogTitle>
<DialogDescription>
<Label>Enter new name</Label>
<Input
value={tabname}
onChange={e => setTabname(e.target.value)}
onKeyPress={e => {
if (e.key === 'Enter') {
handleRenameTab()
}
}}
/>
<ErrorText>{tabnameError}</ErrorText>
</DialogDescription>
<Flex
css={{
marginTop: 25,
justifyContent: 'flex-end',
gap: '$3'
}}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<Button variant="primary" onClick={handleRenameTab}>
Confirm
</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
}}
/>
)
})
: tabs[active] && (
<Fragment key={tabs[active].header || active}>{tabs[active].children}</Fragment>
)}
</>
)
}

View File

@@ -1,9 +1,41 @@
import { styled } from "../stitches.config";
import { styled } from '../stitches.config'
const Text = styled("span", {
fontFamily: "$body",
lineHeight: "$body",
color: "$text",
});
const Text = styled('span', {
fontFamily: '$body',
lineHeight: '$body',
color: '$text',
variants: {
small: {
true: {
fontSize: '$xs'
}
},
muted: {
true: {
color: '$mauve9'
}
},
error: {
true: {
color: '$error'
}
},
warning: {
true: {
color: '$warning'
}
},
monospace: {
true: {
fontFamily: '$monospace'
}
},
block: {
true: {
display: 'block'
}
}
}
})
export default Text;
export default Text

113
components/Textarea.tsx Normal file
View File

@@ -0,0 +1,113 @@
import { styled } from '../stitches.config'
export const Textarea = styled('textarea', {
// Reset
appearance: 'none',
borderWidth: '0',
boxSizing: 'border-box',
fontFamily: 'inherit',
outline: 'none',
width: '100%',
flex: '1',
backgroundColor: '$mauve4',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '$sm',
p: '$2',
fontSize: '$md',
lineHeight: 1,
color: '$mauve12',
boxShadow: `0 0 0 1px $colors$mauve8`,
WebkitTapHighlightColor: 'rgba(0,0,0,0)',
'&::before': {
boxSizing: 'border-box'
},
'&::after': {
boxSizing: 'border-box'
},
fontVariantNumeric: 'tabular-nums',
'&:-webkit-autofill': {
boxShadow: 'inset 0 0 0 1px $colors$blue6, inset 0 0 0 100px $colors$blue3'
},
'&:-webkit-autofill::first-line': {
fontFamily: '$untitled',
color: '$mauve12'
},
'&:focus': {
boxShadow: `0 0 0 1px $colors$mauve10`,
'&:-webkit-autofill': {
boxShadow: `0 0 0 1px $colors$mauve10`
}
},
'&::placeholder': {
color: '$mauve9'
},
'&:disabled': {
pointerEvents: 'none',
backgroundColor: '$mauve2',
color: '$mauve8',
cursor: 'not-allowed',
'&::placeholder': {
color: '$mauve7'
}
},
variants: {
variant: {
ghost: {
boxShadow: 'none',
backgroundColor: 'transparent',
'@hover': {
'&:hover': {
boxShadow: 'inset 0 0 0 1px $colors$mauve7'
}
},
'&:focus': {
backgroundColor: '$loContrast',
boxShadow: `0 0 0 1px $colors$mauve10`
},
'&:disabled': {
backgroundColor: 'transparent'
},
'&:read-only': {
backgroundColor: 'transparent'
}
},
deep: {
backgroundColor: '$deep',
boxShadow: 'none'
}
},
state: {
invalid: {
boxShadow: 'inset 0 0 0 1px $colors$crimson7',
'&:focus': {
boxShadow: 'inset 0px 0px 0px 1px $colors$crimson8, 0px 0px 0px 1px $colors$crimson8'
}
},
valid: {
boxShadow: 'inset 0 0 0 1px $colors$grass7',
'&:focus': {
boxShadow: 'inset 0px 0px 0px 1px $colors$grass8, 0px 0px 0px 1px $colors$grass8'
}
}
},
cursor: {
default: {
cursor: 'default',
'&:focus': {
cursor: 'text'
}
},
text: {
cursor: 'text'
}
}
}
})
export default Textarea

View File

@@ -1,37 +1,34 @@
import { useState, useEffect } from "react";
import { useTheme } from "next-themes";
import { Sun, Moon } from "phosphor-react";
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();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
if (!mounted) return null;
if (!mounted) return null
return (
<Box
<Button
outline
onClick={() => {
setTheme(theme && theme === "light" ? "dark" : "light");
setTheme(theme && theme === 'light' ? 'dark' : 'light')
}}
css={{
display: "flex",
marginLeft: "auto",
cursor: "pointer",
alignItems: "center",
justifyContent: "center",
color: "$muted",
display: 'flex',
marginLeft: 'auto',
cursor: 'pointer',
alignItems: 'center',
justifyContent: 'center',
color: '$muted'
}}
>
{theme === "dark" ? (
<Sun weight="bold" size="16px" />
) : (
<Moon weight="bold" size="16px" />
)}
</Box>
);
};
{theme === 'dark' ? <Sun size="15px" /> : <Moon size="15px" />}
</Button>
)
}
export default ThemeChanger;
export default ThemeChanger

90
components/Tooltip.tsx Normal file
View File

@@ -0,0 +1,90 @@
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(0,0,0,.45), 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,.25), 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<React.ComponentProps<typeof StyledContent> & ITooltip> = ({
children,
content,
open,
defaultOpen = false,
onOpenChange,
...rest
}) => {
return (
<TooltipPrimitive.Root
open={open}
defaultOpen={defaultOpen}
onOpenChange={onOpenChange}
delayDuration={100}
>
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
<StyledContent side="bottom" align="center" {...rest}>
<div dangerouslySetInnerHTML={{ __html: content }} />
<StyledArrow offset={5} width={11} height={5} />
</StyledContent>
</TooltipPrimitive.Root>
)
}
export default Tooltip

View File

@@ -0,0 +1,282 @@
import { Play } from 'phosphor-react'
import { FC, useCallback, useEffect } from 'react'
import { useSnapshot } from 'valtio'
import state from '../../state'
import {
defaultTransactionType,
getTxFields,
modifyTxState,
prepareState,
prepareTransaction,
SelectOption,
TransactionState
} from '../../state/transactions'
import { sendTransaction } from '../../state/actions'
import Box from '../Box'
import Button from '../Button'
import Flex from '../Flex'
import { TxJson } from './json'
import { TxUI } from './ui'
import { default as _estimateFee } from '../../utils/estimateFee'
import toast from 'react-hot-toast'
import { combineFlags, extractFlags, transactionFlags } from '../../state/constants/flags'
import { SetHookData, toHex } from '../../utils/setHook'
export interface TransactionProps {
header: string
state: TransactionState
}
const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props }) => {
const { accounts, editorSettings } = useSnapshot(state)
const { selectedAccount, selectedTransaction, txIsDisabled, txIsLoading, viewType, editorValue } =
txState
const setState = useCallback(
(pTx?: Partial<TransactionState>) => {
return modifyTxState(header, pTx)
},
[header]
)
const prepareOptions = useCallback(
(state: Partial<TransactionState> = txState) => {
const {
selectedTransaction,
selectedDestAccount,
selectedAccount,
txFields,
selectedFlags,
hookParameters,
memos
} = state
const TransactionType = selectedTransaction?.value || null
const Destination = selectedDestAccount?.value || txFields?.Destination
const Account = selectedAccount?.value || null
const Flags = combineFlags(selectedFlags?.map(flag => flag.value)) || txFields?.Flags
const HookParameters = Object.entries(hookParameters || {}).reduce<
SetHookData['HookParameters']
>((acc, [_, { label, value }]) => {
return acc.concat({
HookParameter: { HookParameterName: toHex(label), HookParameterValue: toHex(value) }
})
}, [])
const Memos = memos
? Object.entries(memos).reduce<SetHookData['Memos']>((acc, [_, { format, data, type }]) => {
return acc?.concat({
Memo: { MemoData: toHex(data), MemoFormat: toHex(format), MemoType: toHex(type) }
})
}, [])
: undefined
return prepareTransaction({
...txFields,
HookParameters,
Flags,
TransactionType,
Destination,
Account,
Memos
})
},
[txState]
)
useEffect(() => {
const transactionType = selectedTransaction?.value
const account = selectedAccount?.value
if (!account || !transactionType || txIsLoading) {
setState({ txIsDisabled: true })
} else {
setState({ txIsDisabled: false })
}
}, [selectedAccount?.value, selectedTransaction?.value, setState, txIsLoading])
const getJsonString = useCallback(
(state?: Partial<TransactionState>) =>
JSON.stringify(prepareOptions?.(state) || {}, null, editorSettings.tabSize),
[editorSettings.tabSize, prepareOptions]
)
const saveEditorState = useCallback(
(value: string = '', transactionType?: string) => {
const pTx = prepareState(value, transactionType)
if (pTx) {
pTx.editorValue = getJsonString(pTx)
return setState(pTx)
}
},
[getJsonString, setState]
)
const submitTest = useCallback(async () => {
let st: TransactionState | undefined
const tt = txState.selectedTransaction?.value
if (viewType === 'json') {
st = saveEditorState(editorValue, tt)
if (!st) return
}
const account = accounts.find(acc => acc.address === selectedAccount?.value)
if (txIsDisabled) return
setState({ txIsLoading: true })
const logPrefix = header ? `${header.split('.')[0]}: ` : undefined
try {
if (!account) {
throw Error('Account must be selected from imported accounts!')
}
const options = prepareOptions(st)
const fields = getTxFields(options.TransactionType)
if (fields.Destination && !options.Destination) {
throw Error('Destination account is required!')
}
await sendTransaction(account, options, { logPrefix })
} catch (error) {
console.error(error)
if (error instanceof Error) {
state.transactionLogs.push({
type: 'error',
message: `${logPrefix}${error.message}`
})
}
}
setState({ txIsLoading: false })
}, [
txState.selectedTransaction?.value,
viewType,
accounts,
txIsDisabled,
setState,
header,
saveEditorState,
editorValue,
selectedAccount?.value,
prepareOptions
])
const resetState = useCallback(
(transactionType: SelectOption | undefined = defaultTransactionType) => {
const fields = getTxFields(transactionType?.value)
const nwState: Partial<TransactionState> = {
viewType,
selectedTransaction: transactionType
}
if (fields.Destination !== undefined) {
nwState.selectedDestAccount = null
fields.Destination = ''
} else {
fields.Destination = undefined
}
if (transactionType?.value && transactionFlags[transactionType?.value] && fields.Flags) {
nwState.selectedFlags = extractFlags(transactionType.value, fields.Flags)
fields.Flags = undefined
}
nwState.txFields = fields
const state = modifyTxState(header, nwState, { replaceState: true })
const editorValue = getJsonString(state)
return setState({ editorValue })
},
[getJsonString, header, setState, viewType]
)
const estimateFee = useCallback(
async (st?: TransactionState, opts?: { silent?: boolean }) => {
const state = st || txState
const ptx = prepareOptions(state)
const account = accounts.find(acc => acc.address === state.selectedAccount?.value)
if (!account) {
if (!opts?.silent) {
toast.error('Please select account from the list.')
}
return
}
ptx.Account = account.address
ptx.Sequence = account.sequence
const res = await _estimateFee(ptx, account, opts)
const fee = res?.base_fee
setState({ estimatedFee: fee })
return fee
},
[accounts, prepareOptions, setState, txState]
)
const switchToJson = useCallback(() => {
const editorValue = getJsonString()
setState({ viewType: 'json', editorValue })
}, [getJsonString, setState])
const switchToUI = useCallback(() => {
setState({ viewType: 'ui' })
}, [setState])
return (
<Box css={{ position: 'relative', height: 'calc(100% - 28px)' }} {...props}>
{viewType === 'json' ? (
<TxJson
getJsonString={getJsonString}
saveEditorState={saveEditorState}
header={header}
state={txState}
setState={setState}
estimateFee={estimateFee}
/>
) : (
<TxUI
switchToJson={switchToJson}
state={txState}
resetState={resetState}
setState={setState}
estimateFee={estimateFee}
/>
)}
<Flex
row
css={{
justifyContent: 'space-between',
position: 'absolute',
left: 0,
bottom: 0,
width: '100%',
mb: '$1'
}}
>
<Button
onClick={() => {
if (viewType === 'ui') {
switchToJson()
} else switchToUI()
}}
outline
>
{viewType === 'ui' ? 'EDIT AS JSON' : 'EXIT JSON MODE'}
</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>
)
}
export default Transaction

View File

@@ -0,0 +1,197 @@
import { FC, useCallback, useEffect, useState } from 'react'
import { useSnapshot } from 'valtio'
import state, { transactionsData, TransactionState } from '../../state'
import Text from '../Text'
import { Flex, Link } from '..'
import { showAlert } from '../../state/actions/showAlert'
import { parseJSON } from '../../utils/json'
import { extractSchemaProps } from '../../utils/schema'
import amountSchema from '../../content/amount-schema.json'
import Monaco from '../Monaco'
import type monaco from 'monaco-editor'
interface JsonProps {
getJsonString: (st?: Partial<TransactionState>) => string
saveEditorState: (val?: string, tt?: string) => TransactionState | undefined
header?: string
setState: (pTx?: Partial<TransactionState> | undefined) => void
state: TransactionState
estimateFee?: () => Promise<string | undefined>
}
export const TxJson: FC<JsonProps> = ({
getJsonString,
state: txState,
header,
setState,
saveEditorState
}) => {
const { editorSettings, accounts } = useSnapshot(state)
const { editorValue, estimatedFee, editorIsSaved } = txState
const [currTxType, setCurrTxType] = useState<string | undefined>(
txState.selectedTransaction?.value
)
useEffect(() => {
const parsed = parseJSON(editorValue)
if (!parsed) return
const tt = parsed.TransactionType
const tx = transactionsData.find(t => t.TransactionType === tt)
if (tx) setCurrTxType(tx.TransactionType)
else {
setCurrTxType(undefined)
}
}, [editorValue])
const discardChanges = () => {
showAlert('Confirm', {
body: 'Are you sure to discard these changes?',
confirmText: 'Yes',
onCancel: () => {},
onConfirm: () => setState({ editorValue: getJsonString() })
})
}
const onExit = (value: string) => {
const options = parseJSON(value)
if (options) {
saveEditorState(value, currTxType)
return
}
showAlert('Error!', {
body: `Malformed Transaction in ${header}, would you like to discard these changes?`,
confirmText: 'Discard',
onConfirm: () => setState({ editorValue: getJsonString?.() }),
onCancel: () => setState({ viewType: 'json' })
})
}
const getSchemas = useCallback(async (): Promise<any[]> => {
const txObj = transactionsData.find(td => td.TransactionType === currTxType)
let genericSchemaProps: any
if (txObj) {
genericSchemaProps = extractSchemaProps(txObj)
} else {
genericSchemaProps = transactionsData.reduce(
(cumm, td) => ({
...cumm,
...extractSchemaProps(td)
}),
{}
)
}
return [
{
uri: 'file:///main-schema.json', // id of the first schema
fileMatch: ['**.json'], // associate with our model
schema: {
title: header,
type: 'object',
required: ['TransactionType', 'Account'],
properties: {
...genericSchemaProps,
TransactionType: {
title: 'Transaction Type',
enum: transactionsData.map(td => td.TransactionType)
},
Account: {
$ref: 'file:///account-schema.json'
},
Destination: {
anyOf: [
{
$ref: 'file:///account-schema.json'
},
{
type: 'string',
title: 'Destination Account'
}
]
},
Amount: {
$ref: 'file:///amount-schema.json'
},
Fee: {
$ref: 'file:///fee-schema.json'
}
}
}
},
{
uri: 'file:///account-schema.json',
schema: {
type: 'string',
title: 'Account type',
enum: accounts.map(acc => acc.address)
}
},
{
uri: 'file:///fee-schema.json',
schema: {
type: 'string',
title: 'Fee type',
const: estimatedFee,
description: estimatedFee ? 'Above mentioned value is recommended base fee' : undefined
}
},
{
...amountSchema
}
]
}, [accounts, currTxType, estimatedFee, header])
const [monacoInst, setMonacoInst] = useState<typeof monaco>()
useEffect(() => {
if (!monacoInst) return
getSchemas().then(schemas => {
monacoInst.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
schemas
})
})
}, [getSchemas, monacoInst])
return (
<Monaco
rootProps={{
css: { height: 'calc(100% - 45px)' }
}}
language={'json'}
id={header}
height="100%"
value={editorValue}
onChange={val => setState({ editorValue: val, editorIsSaved: false })}
onMount={(editor, monaco) => {
editor.updateOptions({
minimap: { enabled: false },
glyphMargin: true,
tabSize: editorSettings.tabSize,
dragAndDrop: true,
fontSize: 14
})
setMonacoInst(monaco)
// register onExit cb
const model = editor.getModel()
model?.onWillDispose(() => onExit(model.getValue()))
}}
overlay={
!editorIsSaved ? (
<Flex row align="center" css={{ fontSize: '$xs', color: '$textMuted', ml: 'auto' }}>
<Text muted small>
This file has unsaved changes.
</Text>
<Link css={{ ml: '$1' }} onClick={() => saveEditorState(editorValue, currTxType)}>
save
</Link>
<Link css={{ ml: '$1' }} onClick={discardChanges}>
discard
</Link>
</Flex>
) : undefined
}
/>
)
}

View File

@@ -0,0 +1,452 @@
import { FC, ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
import Container from '../Container'
import Flex from '../Flex'
import Input from '../Input'
import Select from '../Select'
import Text from '../Text'
import {
SelectOption,
TransactionState,
transactionsOptions,
TxFields,
getTxFields,
defaultTransactionType
} from '../../state/transactions'
import { useSnapshot } from 'valtio'
import state from '../../state'
import { streamState } from '../DebugStream'
import { Button } from '..'
import Textarea from '../Textarea'
import { getFlags } from '../../state/constants/flags'
import { Plus, Trash } from 'phosphor-react'
import AccountSequence from '../Sequence'
interface UIProps {
setState: (pTx?: Partial<TransactionState> | undefined) => TransactionState | undefined
resetState: (tt?: SelectOption) => TransactionState | undefined
state: TransactionState
estimateFee?: (...arg: any) => Promise<string | undefined>
switchToJson: () => void
}
export const TxUI: FC<UIProps> = ({
state: txState,
setState,
resetState,
estimateFee,
switchToJson
}) => {
const { accounts } = useSnapshot(state)
const {
selectedAccount,
selectedDestAccount,
selectedTransaction,
txFields,
selectedFlags,
hookParameters,
memos
} = txState
const accountOptions: SelectOption[] = accounts.map(acc => ({
label: acc.name,
value: acc.address
}))
const destAccountOptions: SelectOption[] = accounts
.map(acc => ({
label: acc.name,
value: acc.address
}))
.filter(acc => acc.value !== selectedAccount?.value)
const flagsOptions: SelectOption[] = Object.entries(
getFlags(selectedTransaction?.value) || {}
).map(([label, value]) => ({
label,
value
}))
const [feeLoading, setFeeLoading] = useState(false)
const handleSetAccount = (acc: SelectOption) => {
setState({ selectedAccount: acc })
streamState.selectedAccount = acc
}
const handleSetField = useCallback(
(field: keyof TxFields, value: string, opFields?: TxFields) => {
const fields = opFields || txFields
const obj = fields[field]
setState({
txFields: {
...fields,
[field]: typeof obj === 'object' ? { ...obj, $value: value } : value
}
})
},
[setState, txFields]
)
const handleEstimateFee = useCallback(
async (state?: TransactionState, silent?: boolean) => {
setFeeLoading(true)
const fee = await estimateFee?.(state, { silent })
if (fee) handleSetField('Fee', fee, state?.txFields)
setFeeLoading(false)
},
[estimateFee, handleSetField]
)
const handleChangeTxType = useCallback(
(tt: SelectOption) => {
setState({ selectedTransaction: tt })
const newState = resetState(tt)
handleEstimateFee(newState, true)
},
[handleEstimateFee, resetState, setState]
)
// default tx
useEffect(() => {
if (selectedTransaction?.value) return
if (defaultTransactionType) {
handleChangeTxType(defaultTransactionType)
}
}, [handleChangeTxType, selectedTransaction?.value])
const fields = useMemo(
() => getTxFields(selectedTransaction?.value),
[selectedTransaction?.value]
)
const richFields = ['TransactionType', 'Account', 'HookParameters', 'Memos']
if (fields.Destination !== undefined) {
richFields.push('Destination')
}
if (flagsOptions.length) {
richFields.push('Flags')
}
const otherFields = Object.keys(txFields).filter(k => !richFields.includes(k)) as [keyof TxFields]
return (
<Container
css={{
p: '$3 01',
fontSize: '$sm',
height: 'calc(100% - 45px)'
}}
>
<Flex column fluid css={{ height: '100%', overflowY: 'auto', pr: '$1' }}>
<TxField label="Transaction type">
<Select
instanceId="transactionsType"
placeholder="Select transaction type"
options={transactionsOptions}
hideSelectedOptions
value={selectedTransaction}
onChange={(tt: any) => handleChangeTxType(tt)}
/>
</TxField>
<TxField label="Account">
<Select
instanceId="from-account"
placeholder="Select your account"
options={accountOptions}
value={selectedAccount}
onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc
/>
</TxField>
<TxField label="Sequence">
<AccountSequence address={selectedAccount?.value} />
</TxField>
{richFields.includes('Destination') && (
<TxField label="Destination account">
<Select
instanceId="to-account"
placeholder="Select the destination account"
options={destAccountOptions}
value={selectedDestAccount}
isClearable
onChange={(acc: any) => setState({ selectedDestAccount: acc })}
/>
</TxField>
)}
{richFields.includes('Flags') && (
<TxField label="Flags">
<Select
isClearable
instanceId="flags"
placeholder="Select flags to apply"
menuPosition="fixed"
value={selectedFlags}
isMulti
options={flagsOptions}
onChange={flags => setState({ selectedFlags: flags as any })}
closeMenuOnSelect={
selectedFlags ? selectedFlags.length >= flagsOptions.length - 1 : false
}
/>
</TxField>
)}
{otherFields.map(field => {
let _value = txFields[field]
let value: string | undefined
if (typeof _value === 'object') {
if (_value.$type === 'json' && typeof _value.$value === 'object') {
value = JSON.stringify(_value.$value, null, 2)
} else {
value = _value.$value.toString()
}
} else {
value = _value?.toString()
}
const isXrp = typeof _value === 'object' && _value.$type === 'xrp'
const isJson = typeof _value === 'object' && _value.$type === 'json'
const isFee = field === 'Fee'
let rows = isJson ? (value?.match(/\n/gm)?.length || 0) + 1 : undefined
if (rows && rows > 5) rows = 5
return (
<TxField key={field} label={field + (isXrp ? ' (XRP)' : '')}>
{isJson ? (
<Textarea
rows={rows}
value={value}
spellCheck={false}
onChange={switchToJson}
css={{
flex: 'inherit',
resize: 'vertical'
}}
/>
) : (
<Input
type={isFee ? 'number' : 'text'}
value={value}
onChange={e => {
if (isFee) {
const val = e.target.value.replaceAll('.', '').replaceAll(',', '')
handleSetField(field, val)
} else {
handleSetField(field, e.target.value)
}
}}
onKeyPress={
isFee
? e => {
if (e.key === '.' || e.key === ',') {
e.preventDefault()
}
}
: undefined
}
css={{
flex: 'inherit',
'-moz-appearance': 'textfield',
'&::-webkit-outer-spin-button': {
'-webkit-appearance': 'none',
margin: 0
},
'&::-webkit-inner-spin-button ': {
'-webkit-appearance': 'none',
margin: 0
}
}}
/>
)}
{isFee && (
<Button
size="xs"
variant="primary"
outline
disabled={txState.txIsDisabled}
isDisabled={txState.txIsDisabled}
isLoading={feeLoading}
css={{
position: 'absolute',
right: '$2',
fontSize: '$xs',
cursor: 'pointer',
alignContent: 'center',
display: 'flex'
}}
onClick={() => handleEstimateFee()}
>
Suggest
</Button>
)}
</TxField>
)
})}
<TxField multiLine label="Hook parameters">
<Flex column fluid>
{Object.entries(hookParameters).map(([id, { label, value }]) => (
<Flex column key={id} css={{ mb: '$2' }}>
<Flex row>
<Input
placeholder="Parameter name"
value={label}
onChange={e => {
setState({
hookParameters: {
...hookParameters,
[id]: { label: e.target.value, value }
}
})
}}
/>
<Input
css={{ mx: '$2' }}
placeholder="Value"
value={value}
onChange={e => {
setState({
hookParameters: {
...hookParameters,
[id]: { label, value: e.target.value }
}
})
}}
/>
<Button
onClick={() => {
const { [id]: _, ...rest } = hookParameters
setState({ hookParameters: rest })
}}
variant="destroy"
>
<Trash weight="regular" size="16px" />
</Button>
</Flex>
</Flex>
))}
<Button
outline
fullWidth
type="button"
onClick={() => {
const id = Object.keys(hookParameters).length
setState({
hookParameters: { ...hookParameters, [id]: { label: '', value: '' } }
})
}}
>
<Plus size="16px" />
Add Hook Parameter
</Button>
</Flex>
</TxField>
<TxField multiLine label="Memos">
<Flex column fluid>
{Object.entries(memos).map(([id, memo]) => (
<Flex column key={id} css={{ mb: '$2' }}>
<Flex
row
css={{
flexWrap: 'wrap',
width: '100%'
}}
>
<Input
placeholder="Memo type"
value={memo.type}
onChange={e => {
setState({
memos: {
...memos,
[id]: { ...memo, type: e.target.value }
}
})
}}
/>
<Input
placeholder="Data"
css={{ mx: '$2' }}
value={memo.data}
onChange={e => {
setState({
memos: {
...memos,
[id]: { ...memo, data: e.target.value }
}
})
}}
/>
<Input
placeholder="Format"
value={memo.format}
onChange={e => {
setState({
memos: {
...memos,
[id]: { ...memo, format: e.target.value }
}
})
}}
/>
<Button
css={{ ml: '$2' }}
onClick={() => {
const { [id]: _, ...rest } = memos
setState({ memos: rest })
}}
variant="destroy"
>
<Trash weight="regular" size="16px" />
</Button>
</Flex>
</Flex>
))}
<Button
outline
fullWidth
type="button"
onClick={() => {
const id = Object.keys(memos).length
setState({
memos: { ...memos, [id]: { data: '', format: '', type: '' } }
})
}}
>
<Plus size="16px" />
Add Memo
</Button>
</Flex>
</TxField>
</Flex>
</Container>
)
}
export const TxField: FC<{ label: string; children: ReactNode; multiLine?: boolean }> = ({
label,
children,
multiLine = false
}) => {
return (
<Flex
row
fluid
css={{
justifyContent: 'flex-end',
alignItems: multiLine ? 'flex-start' : 'center',
position: 'relative',
mb: '$3',
mt: '1px',
pr: '1px'
}}
>
<Text muted css={{ mr: '$3', mt: multiLine ? '$2' : 0 }}>
{label}:{' '}
</Text>
<Flex css={{ width: '70%', alignItems: 'center' }}>{children}</Flex>
</Flex>
)
}

View File

@@ -0,0 +1,34 @@
const Carbon = () => (
<svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M33 2L23 15H28L21 24H45L38 15H43L33 2Z"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M33 24V30"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M-1.14441e-05 4L8.94099 15.0625L4.00543e-05 26.125H2.27587L10.5015 15.9475H16.5938V14.1775H10.5015L2.27582 4H-1.14441e-05Z"
fill="#EDEDEF"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M66 4L57.059 15.0625L66 26.125H63.7241L55.4985 15.9475H49.4062V14.1775H55.4985L63.7242 4H66Z"
fill="#EDEDEF"
/>
</svg>
)
export default Carbon

View File

@@ -0,0 +1,69 @@
const Firewall = () => (
<svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M33 13V7"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M27 19V13"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M39 19V13"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M33 25V19"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M21 13H45"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M21 19H45"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M45 7H21V25H45V7Z"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M-1.14441e-05 4.875L8.94099 15.9375L4.00543e-05 27H2.27587L10.5015 16.8225H16.5938V15.0525H10.5015L2.27582 4.875H-1.14441e-05Z"
fill="#EDEDEF"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M66 4.875L57.059 15.9375L66 27H63.7241L55.4985 16.8225H49.4062V15.0525H55.4985L63.7242 4.875H66Z"
fill="#EDEDEF"
/>
</svg>
)
export default Firewall

View File

@@ -0,0 +1,34 @@
const Notary = () => (
<svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M37.5 10.5L26.5 21.5L21 16.0002"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M49 10.5L38 21.5L35.0784 18.5785"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M-1.14441e-05 5L8.94099 16.0625L4.00543e-05 27.125H2.27587L10.5015 16.9475H16.5938V15.1775H10.5015L2.27582 5H-1.14441e-05Z"
fill="#EDEDEF"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M66 5L57.059 16.0625L66 27.125H63.7241L55.4985 16.9475H49.4062V15.1775H55.4985L63.7242 5H66Z"
fill="#EDEDEF"
/>
</svg>
)
export default Notary

View File

@@ -0,0 +1,55 @@
const Peggy = () => (
<svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M33 19C40.1797 19 46 16.3137 46 13C46 9.68629 40.1797 7 33 7C25.8203 7 20 9.68629 20 13C20 16.3137 25.8203 19 33 19Z"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M33 19V25"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M20 13V19C20 22 25 25 33 25C41 25 46 22 46 19V13"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M41 17.7633V23.7634"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M25 17.7633V23.7634"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M-1.14441e-05 4L8.94099 15.0625L4.00543e-05 26.125H2.27587L10.5015 15.9475H16.5938V14.1775H10.5015L2.27582 4H-1.14441e-05Z"
fill="#EDEDEF"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M66 4L57.059 15.0625L66 26.125H63.7241L55.4985 15.9475H49.4062V14.1775H55.4985L63.7242 4H66Z"
fill="#EDEDEF"
/>
</svg>
)
export default Peggy

View File

@@ -0,0 +1,34 @@
const Starter = () => (
<svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M42 28H24C23.7347 28 23.4804 27.8946 23.2929 27.7071C23.1053 27.5196 23 27.2652 23 27V5C23 4.73479 23.1053 4.48044 23.2929 4.2929C23.4804 4.10537 23.7347 4.00001 24 4H36.0003L43 11V27C43 27.2652 42.8947 27.5196 42.7071 27.7071C42.5196 27.8946 42.2653 28 42 28V28Z"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M36 4V11H43.001"
stroke="#EDEDEF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M-1.14441e-05 4.875L8.94099 15.9375L4.00543e-05 27H2.27587L10.5015 16.8225H16.5938V15.0525H10.5015L2.27582 4.875H-1.14441e-05Z"
fill="#EDEDEF"
/>
<path
className="angle"
fillRule="evenodd"
clipRule="evenodd"
d="M66 4.875L57.059 15.9375L66 27H63.7241L55.4985 16.8225H49.4062V15.0525H55.4985L63.7242 4.875H66Z"
fill="#EDEDEF"
/>
</svg>
)
export default Starter

16
components/index.tsx Normal file
View File

@@ -0,0 +1,16 @@
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, Label } from './Input'
export { default as Select } from './Select'
export * from './Tabs'
export * from './AlertDialog/primitive'
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 * from './Dialog'
export * from './DropdownMenu'

View File

@@ -0,0 +1,44 @@
{
"uri": "file:///amount-schema.json",
"title": "Amount",
"description": "Specify xrp in drops and tokens as objects.",
"schema": {
"anyOf": [
{
"type": ["number", "string"],
"exclusiveMinimum": 0,
"maximum": "100000000000000000"
},
{
"type": "object",
"properties": {
"currency": {
"description": "Arbitrary currency code for the token. Cannot be XRP."
},
"value": {
"type": ["string", "number"],
"description": "Quoted decimal representation of the amount of the token."
},
"issuer": {
"type": "string",
"description": "Generally, the account that issues this token. In special cases, this can refer to the account that holds the token instead."
}
}
}
],
"defaultSnippets": [
{
"label": "Xrp",
"body": "1000000"
},
{
"label": "Token",
"body": {
"currency": "${1:USD}",
"value": "${2:100}",
"issuer": "${3:rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpns}"
}
}
]
}
}

409
content/hook-set-codes.json Normal file
View File

@@ -0,0 +1,409 @@
[
{
"code": 1,
"identifier": "AMENDMENT_DISABLED",
"description": "attempt to HookSet when amendment is not yet enabled."
},
{
"code": 2,
"identifier": "API_ILLEGAL",
"description": "HookSet object contained HookApiVersion for existing HookDefinition"
},
{
"code": 3,
"identifier": "API_INVALID",
"description": "HookSet object contained HookApiVersion for unrecognised hook API "
},
{
"code": 4,
"identifier": "API_MISSING",
"description": "HookSet object lacked HookApiVersion"
},
{
"code": 5,
"identifier": "BLOCK_ILLEGAL",
"description": " a block end instruction moves execution below depth 0 {{}}`}` <= like this"
},
{
"code": 6,
"identifier": "CALL_ILLEGAL",
"description": "wasm tries to call a non-whitelisted function"
},
{
"code": 7,
"identifier": "CALL_INDIRECT",
"description": "wasm used call indirect instruction which is disallowed"
},
{
"code": 8,
"identifier": "CREATE_FLAG",
"description": "create operation requires hsoOVERRIDE"
},
{
"code": 9,
"identifier": "DELETE_FIELD",
"description": ""
},
{
"code": 10,
"identifier": "DELETE_FLAG",
"description": "delete operation requires hsoOVERRIDE"
},
{
"code": 11,
"identifier": "DELETE_NOTHING",
"description": "delete operation would delete nothing"
},
{
"code": 12,
"identifier": "EXPORTS_MISSING",
"description": "hook did not export *any* functions (should be cbak, hook)"
},
{
"code": 13,
"identifier": "EXPORT_CBAK_FUNC",
"description": "hook did not export correct func def int64_t cbak(uint32_t)"
},
{
"code": 14,
"identifier": "EXPORT_HOOK_FUNC",
"description": "hook did not export correct func def int64_t hook(uint32_t)"
},
{
"code": 15,
"identifier": "EXPORT_MISSING",
"description": "distinct from export*S*_missing, either hook or cbak is missing"
},
{
"identifier": "FLAGS_INVALID",
"code": 16,
"description": "HookSet flags were invalid for specified operation "
},
{
"identifier": "FUNCS_MISSING",
"code": 17,
"description": "hook did not include function code for any functions "
},
{
"identifier": "FUNC_PARAM_INVALID",
"code": 18,
"description": "parameter types may only be i32 i64 u32 u64 "
},
{
"identifier": "FUNC_RETURN_COUNT",
"code": 19,
"description": "a function type is defined in the wasm which returns > 1 return value "
},
{
"identifier": "FUNC_RETURN_INVALID",
"code": 20,
"description": "a function type does not return i32 i64 u32 or u64 "
},
{
"identifier": "FUNC_TYPELESS",
"code": 21,
"description": "hook defined hook/cbak but their type is not defined in wasm "
},
{
"identifier": "FUNC_TYPE_INVALID",
"code": 22,
"description": "malformed and illegal wasm in the func type section "
},
{
"identifier": "GRANTS_EMPTY",
"code": 23,
"description": "HookSet object contained an empty grants array (you should remove it) "
},
{
"identifier": "GRANTS_EXCESS",
"code": 24,
"description": "HookSet object cotnained a grants array with too many grants "
},
{
"identifier": "GRANTS_FIELD",
"code": 25,
"description": "HookSet object contained a grant without Authorize or HookHash "
},
{
"identifier": "GRANTS_ILLEGAL",
"code": 26,
"description": "Hookset object contained grants array which contained a non Grant object "
},
{
"identifier": "GUARD_IMPORT",
"code": 27,
"description": "guard import is missing "
},
{
"identifier": "GUARD_MISSING",
"code": 28,
"description": "guard call missing at top of loop "
},
{
"identifier": "GUARD_PARAMETERS",
"code": 29,
"description": "guard called but did not use constant expressions for params "
},
{
"identifier": "HASH_OR_CODE",
"code": 30,
"description": "HookSet object can contain only one of CreateCode and HookHash "
},
{
"identifier": "HOOKON_MISSING",
"code": 31,
"description": "HookSet object did not contain HookOn but should have "
},
{
"identifier": "HOOKS_ARRAY_BAD",
"code": 32,
"description": "attempt to HookSet with a Hooks array containing a non-Hook obj "
},
{
"identifier": "HOOKS_ARRAY_BLANK",
"code": 33,
"description": "all hook set objs were blank "
},
{
"identifier": "HOOKS_ARRAY_EMPTY",
"code": 34,
"description": "attempt to HookSet with an empty Hooks array "
},
{
"identifier": "HOOKS_ARRAY_MISSING",
"code": 35,
"description": "attempt to HookSet without a Hooks array "
},
{
"identifier": "HOOKS_ARRAY_TOO_BIG",
"code": 36,
"description": "attempt to HookSet with a Hooks array beyond the chain size limit "
},
{
"identifier": "HOOK_ADD",
"code": 37,
"description": "Informational: adding ltHook to directory "
},
{
"identifier": "HOOK_DEF_MISSING",
"code": 38,
"description": "attempt to reference a hook definition (by hash) that is not on ledger "
},
{
"identifier": "HOOK_DELETE",
"code": 39,
"description": "unable to delete ltHook from owner "
},
{
"identifier": "HOOK_INVALID_FIELD",
"code": 40,
"description": "HookSetObj contained an illegal/unexpected field "
},
{
"identifier": "HOOK_PARAMS_COUNT",
"code": 41,
"description": "hookset obj would create too many hook parameters "
},
{
"identifier": "HOOK_PARAM_SIZE",
"code": 42,
"description": "hookset obj sets a parameter or value that exceeds max allowable size "
},
{
"identifier": "IMPORTS_MISSING",
"code": 43,
"description": "hook must import guard, and accept/rollback "
},
{
"identifier": "IMPORT_ILLEGAL",
"code": 44,
"description": "attempted import of a non-whitelisted function "
},
{
"identifier": "IMPORT_MODULE_BAD",
"code": 45,
"description": "hook attempted to specify no or a bad import module "
},
{
"identifier": "IMPORT_MODULE_ENV",
"code": 46,
"description": "hook attempted to specify import module not named env "
},
{
"identifier": "IMPORT_NAME_BAD",
"code": 47,
"description": "import name was too short or too long "
},
{
"identifier": "INSTALL_FLAG",
"code": 48,
"description": "install operation requires hsoOVERRIDE "
},
{
"identifier": "INSTALL_MISSING",
"code": 49,
"description": "install operation specifies hookhash which doesn't exist on the ledger "
},
{
"identifier": "INSTRUCTION_COUNT",
"code": 50,
"description": "worst case execution instruction count as computed by HookSet "
},
{
"identifier": "INSTRUCTION_EXCESS",
"code": 51,
"description": "worst case execution instruction count was too large "
},
{
"identifier": "MEMORY_GROW",
"code": 52,
"description": "memory.grow instruction is present but disallowed "
},
{
"identifier": "NAMESPACE_MISSING",
"code": 53,
"description": "HookSet object lacked HookNamespace "
},
{
"identifier": "NSDELETE",
"code": 54,
"description": "Informational: a namespace is being deleted "
},
{
"identifier": "NSDELETE_ACCOUNT",
"code": 55,
"description": "nsdelete tried to delete ns from a non-existing account "
},
{
"identifier": "NSDELETE_COUNT",
"code": 56,
"description": "namespace state count less than 0 / overflow "
},
{
"identifier": "NSDELETE_DIR",
"code": 57,
"description": "could not delete directory node in ledger "
},
{
"identifier": "NSDELETE_DIRECTORY",
"code": 58,
"description": "nsdelete operation failed to delete ns directory "
},
{
"identifier": "NSDELETE_DIR_ENTRY",
"code": 59,
"description": "nsdelete operation failed due to bad entry in ns directory "
},
{
"identifier": "NSDELETE_ENTRY",
"code": 60,
"description": "nsdelete operation failed due to missing hook state entry "
},
{
"identifier": "NSDELETE_FIELD",
"code": 61
},
{
"identifier": "NSDELETE_FLAGS",
"code": 62
},
{
"identifier": "NSDELETE_NONSTATE",
"code": 63,
"description": "nsdelete operation failed due to the presence of a non-hookstate obj "
},
{
"identifier": "NSDELETE_NOTHING",
"code": 64,
"description": "hsfNSDELETE provided but nothing to delete "
},
{
"identifier": "OPERATION_INVALID",
"code": 65,
"description": "could not deduce an operation from the provided hookset obj "
},
{
"identifier": "OVERRIDE_MISSING",
"code": 66,
"description": "HookSet object was trying to update or delete a hook but lacked hsfOVERRIDE "
},
{
"identifier": "PARAMETERS_FIELD",
"code": 67,
"description": "HookParameters contained a HookParameter with an invalid key in it "
},
{
"identifier": "PARAMETERS_ILLEGAL",
"code": 68,
"description": "HookParameters contained something other than a HookParameter "
},
{
"identifier": "PARAMETERS_NAME",
"code": 69,
"description": "HookParameters contained a HookParameter which lacked ParameterName field "
},
{
"identifier": "PARAM_HOOK_CBAK",
"code": 70,
"description": "hook and cbak must take exactly one u32 parameter "
},
{
"identifier": "RETURN_HOOK_CBAK",
"code": 71,
"description": "hook and cbak must retunr i64 "
},
{
"identifier": "SHORT_HOOK",
"code": 72,
"description": "web assembly byte code ended abruptly "
},
{
"identifier": "TYPE_INVALID",
"code": 73,
"description": "malformed and illegal wasm specifying an illegal local var type "
},
{
"identifier": "WASM_BAD_MAGIC",
"code": 74,
"description": "wasm magic number missing or not wasm "
},
{
"identifier": "WASM_INVALID",
"code": 75,
"description": "set hook operation would set invalid wasm "
},
{
"identifier": "WASM_PARSE_LOOP",
"code": 76,
"description": "wasm section parsing resulted in an infinite loop "
},
{
"identifier": "WASM_SMOKE_TEST",
"code": 77,
"description": "Informational: first attempt to load wasm into wasm runtime "
},
{
"identifier": "WASM_TEST_FAILURE",
"code": 78,
"description": "the smoke test failed "
},
{
"identifier": "WASM_TOO_BIG",
"code": 79,
"description": "set hook would exceed maximum hook size "
},
{
"identifier": "WASM_TOO_SMALL",
"code": 80
},
{
"identifier": "WASM_VALIDATION",
"code": 81,
"description": "a generic error while parsing wasm, usually leb128 overflow"
},
{
"identifier": "HOOK_CBAK_DIFF_TYPES",
"code": 82,
"description": "hook and cbak function definitions were different"
}
]

258
content/transactions.json Normal file
View File

@@ -0,0 +1,258 @@
[
{
"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": "xrp"
},
"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,
"Fee": "10"
},
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowCreate",
"Amount": {
"$value": "100",
"$type": "xrp"
},
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"CancelAfter": 533257958,
"FinishAfter": 533171558,
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
"DestinationTag": 23480,
"SourceTag": 11747,
"Fee": "10"
},
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowFinish",
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"OfferSequence": 7,
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
"Fulfillment": "A0028000",
"Fee": "10"
},
{
"TransactionType": "NFTokenMint",
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
"Fee": "10",
"NFTokenTaxon": 0,
"URI": "697066733A2F2F516D614374444B5A4656767666756676626479346573745A626851483744586831364354707631686F776D424779"
},
{
"TransactionType": "NFTokenBurn",
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
"Fee": "10",
"NFTokenID": "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65"
},
{
"TransactionType": "NFTokenAcceptOffer",
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
"Fee": "10",
"NFTokenSellOffer": "A2FA1A9911FE2AEF83DAB05F437768E26A301EF899BD31EB85E704B3D528FF18",
"NFTokenBuyOffer": "4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862",
"NFTokenBrokerFee": "10"
},
{
"TransactionType": "NFTokenCancelOffer",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Fee": "10",
"NFTokenOffers": {
"$type": "json",
"$value": [
"4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862"
]
}
},
{
"TransactionType": "NFTokenCreateOffer",
"Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX",
"NFTokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
"Amount": {
"$value": "100",
"$type": "xrp"
},
"Flags": "1",
"Destination": "",
"Fee": "10"
},
{
"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": "xrp"
}
},
{
"TransactionType": "Payment",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Amount": {
"$value": "100",
"$type": "xrp"
},
"Fee": "12",
"Flags": "2147483648",
"Sequence": 2
},
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "PaymentChannelCreate",
"Amount": {
"$value": "100",
"$type": "xrp"
},
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"SettleDelay": 86400,
"PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A",
"CancelAfter": 533171558,
"DestinationTag": 23480,
"SourceTag": 11747,
"Fee": "10"
},
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "PaymentChannelFund",
"Channel": "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198",
"Amount": {
"$value": "200",
"$type": "xrp"
},
"Expiration": 543171558,
"Fee": "10"
},
{
"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,
"LimitAmount": {
"$type": "json",
"$value": {
"currency": "USD",
"issuer": "rsP3mgGb2tcYUrxiLFiHJiQXhsziegtwBc",
"value": "100"
}
},
"Sequence": 12
},
{
"TransactionType": "Invoke",
"Fee": "12"
},
{
"TransactionType": "UriToken",
"Fee": "12",
"URI": "697066733A2F2F516D614374444B5A4656767666756676626479346573745A626851483744586831364354707631686F776D424779"
}
]

View File

@@ -1,9 +1,9 @@
import { useEffect, useState } from "react";
import { useEffect, useState } from 'react'
// Define general type for useWindowSize hook, which includes width and height
interface Size {
width: number | undefined;
height: number | undefined;
width: number | undefined
height: number | undefined
}
// Hook
@@ -12,25 +12,25 @@ function useWindowSize(): Size {
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
const [windowSize, setWindowSize] = useState<Size>({
width: undefined,
height: undefined,
});
height: undefined
})
useEffect(() => {
// Handler to call on window resize
function handleResize() {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
height: window.innerHeight
})
}
// Add event listener
window.addEventListener("resize", handleResize);
window.addEventListener('resize', handleResize)
// Call handler right away so state gets updated with initial window size
handleResize();
handleResize()
// Remove event listener on cleanup
return () => window.removeEventListener("resize", handleResize);
}, []); // Empty array ensures that effect is only run on mount
return windowSize;
return () => window.removeEventListener('resize', handleResize)
}, []) // Empty array ensures that effect is only run on mount
return windowSize
}
export default useWindowSize;
export default useWindowSize

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$/, /hook-bundle\.js$/],
use: 'raw-loader'
})
return config
}
}

View File

@@ -6,30 +6,83 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"format": "prettier --write .",
"postinstall": "patch-package && yarn run postinstall-postinstall",
"postinstall-postinstall": "./node_modules/.bin/browserify -r ripple-binary-codec -r ripple-keypairs -r ripple-address-codec -r ripple-secret-codec -r ./node_modules/xrpl-accountlib/dist/index.js:xrpl-accountlib -o node_modules/xrpl-accountlib/dist/browser.hook-bundle.js"
},
"dependencies": {
"@monaco-editor/react": "^4.3.1",
"@codingame/monaco-jsonrpc": "^0.3.1",
"@codingame/monaco-languageclient": "^0.17.0",
"@monaco-editor/react": "^4.4.5",
"@octokit/core": "^3.5.1",
"@radix-ui/colors": "^0.1.7",
"@radix-ui/react-alert-dialog": "^0.1.1",
"@radix-ui/react-context-menu": "^0.1.6",
"@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-dropdown-menu": "^0.1.6",
"@radix-ui/react-id": "^0.1.1",
"@radix-ui/react-label": "^0.1.5",
"@radix-ui/react-popover": "^0.1.6",
"@radix-ui/react-switch": "^0.1.5",
"@radix-ui/react-tooltip": "^0.1.7",
"@stitches/react": "^1.2.8",
"base64-js": "^1.5.1",
"comment-parser": "^1.3.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.33.0",
"next": "^12.0.4",
"next-auth": "^4.0.0-beta.5",
"next-themes": "^0.0.15",
"next-auth": "^4.10.3",
"next-plausible": "^3.2.0",
"next-themes": "^0.1.1",
"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",
"prettier": "^2.7.1",
"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-markdown": "^8.0.3",
"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.30",
"xrpl-accountlib": "^1.6.1",
"xrpl-client": "^2.0.2"
},
"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",
"browserify": "^17.0.0",
"eslint": "7.32.0",
"eslint-config-next": "11.1.2",
"raw-loader": "^4.0.2",
"typescript": "4.4.4"
},
"resolutions": {
"ripple-binary-codec": "=1.4.2"
}
}

View File

@@ -1,31 +1,141 @@
import "../styles/globals.css";
import type { AppProps } from "next/app";
import { SessionProvider } from "next-auth/react";
import { ThemeProvider } from "next-themes";
import { Toaster } from "react-hot-toast";
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 PlausibleProvider from 'next-plausible'
import { darkTheme } from "../stitches.config";
import Navigation from "../components/Navigation";
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'
import Alert from '../components/AlertDialog'
import { Button, Flex } from '../components'
import { ChatCircleText } from 'phosphor-react'
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('/develop') &&
!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 Builder</title>
<meta property="og:title" content="XRPL Hooks Builder" />
<meta name="twitter:title" content="XRPL Hooks Builder" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@XRPLF" />
<meta
name="description"
content="Hooks Builder, add smart contract functionality to the XRP Ledger."
/>
<meta
property="og:description"
content="Hooks Builder, add smart contract functionality to the XRP Ledger."
/>
<meta
name="twitter:description"
content="Hooks Builder, 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 Builder" />
<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
}}
>
<PlausibleProvider domain="hooks-builder.xrpl.org" trackOutboundLinks>
<Navigation />
<Component {...pageProps} />
<Toaster
toastOptions={{
className: css({
backgroundColor: '$mauve1',
color: '$mauve10',
fontSize: '$sm',
zIndex: 9999,
'.dark &': {
backgroundColor: '$mauve4',
color: '$mauve12'
}
})()
}}
/>
<Alert />
<Flex
as="a"
href="https://github.com/XRPLF/Hooks/discussions"
target="_blank"
rel="noopener noreferrer"
css={{ position: 'fixed', right: '$4', bottom: '$4' }}
>
<Button size="sm" variant="primary" outline>
<ChatCircleText size={14} style={{ marginRight: '0px' }} />
Bugs & Discussions
</Button>
</Flex>
</PlausibleProvider>
</ThemeProvider>
</SessionProvider>
</IdProvider>
</>
);
)
}
export default MyApp;
export default MyApp

View File

@@ -1,48 +1,34 @@
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext,
} from "next/document";
import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'
import { globalStyles, getCssText } from "../stitches.config";
import { globalStyles, getCssText } from '../stitches.config'
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx);
const initialProps = await Document.getInitialProps(ctx)
return initialProps;
return initialProps
}
render() {
globalStyles();
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"
href="https://fonts.gstatic.com"
crossOrigin=""
/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
<link
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 />
<NextScript />
</body>
</Html>
);
)
}
}
export default MyDocument;
export default MyDocument

View File

@@ -1,11 +1,10 @@
import type { NextRequest, NextFetchEvent } from 'next/server';
import { NextResponse as Response } from 'next/server';
import { getToken } from "next-auth/jwt"
import type { NextRequest, NextFetchEvent } from 'next/server'
import { NextResponse as Response } from 'next/server'
export default function middleware(req: NextRequest, ev: NextFetchEvent) {
if (req.nextUrl.pathname === "/") {
return Response.redirect("/develop");
if (req.nextUrl.pathname === '/') {
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"
import NextAuth from 'next-auth'
export default NextAuth({
// Configure one or more authentication providers
@@ -11,40 +10,38 @@ export default NextAuth({
// scope: 'user,gist'
// }),
{
id: "github",
name: "GitHub",
type: "oauth",
id: 'github',
name: 'GitHub',
type: 'oauth',
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
authorization: "https://github.com/login/oauth/authorize?scope=read:user+user:email+gist",
token: "https://github.com/login/oauth/access_token",
userinfo: "https://api.github.com/user",
authorization: 'https://github.com/login/oauth/authorize?scope=read:user+user:email+gist',
token: 'https://github.com/login/oauth/access_token',
userinfo: 'https://api.github.com/user',
profile(profile) {
return {
id: profile.id.toString(),
name: profile.name || profile.login,
username: profile.login,
email: profile.email,
image: profile.avatar_url,
image: profile.avatar_url
}
},
}
}
// ...add more providers here
],
callbacks: {
async jwt({ token, user, account, profile, isNewUser }) {
if (account && account.access_token) {
token.accessToken = account.access_token;
token.username = user?.username || '';
token.accessToken = account.access_token
token.username = user?.username || ''
}
return token
},
async session({ session, token }) {
session.accessToken = token.accessToken as string;
const user = { ...session.user, username: token.username };
session['user']['username'] = token.username as string;
session.accessToken = token.accessToken as string
session['user']['username'] = token.username as string
return session
}
},
}
})

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

@@ -0,0 +1,47 @@
// 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

@@ -5,9 +5,6 @@ type Data = {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
export default function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
res.status(200).json({ name: 'John Doe' })
}

15
pages/api/proxy.ts Normal file
View File

@@ -0,0 +1,15 @@
import type { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { url, opts } = req.body
const r = await fetch(url, opts)
if (!r.ok) throw r.statusText
const data = await r.json()
return res.json(data)
} catch (error) {
console.warn(error)
return res.status(500).json({ message: 'Something went wrong!' })
}
}

View File

@@ -0,0 +1,59 @@
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,251 @@
import { Label } from '@radix-ui/react-label'
import type { NextPage } from 'next'
import dynamic from 'next/dynamic'
import { FileJs, Gear, Play } from 'phosphor-react'
import Hotkeys from 'react-hot-keys'
import Split from 'react-split'
import { useSnapshot } from 'valtio'
import { ButtonGroup, Flex } from '../../components'
import Box from '../../components/Box'
import Button from '../../components/Button'
import Popover from '../../components/Popover'
import RunScript from '../../components/RunScript'
import state, { IFile } from '../../state'
import { compileCode } from '../../state/actions'
import { getSplit, saveSplit } from '../../state/actions/persistSplits'
import { styled } from '../../stitches.config'
import { getFileExtention } from '../../utils/helpers'
const HooksEditor = dynamic(() => import('../../components/HooksEditor'), {
ssr: false
})
const LogBox = dynamic(() => import('../../components/LogBox'), {
ssr: false
})
const OptimizationText = () => (
<span>
Specify which optimization level to use for compiling. For example -O0 means no optimization:
this level compiles the fastest and generates the most debuggable code. -O2 means moderate level
of optimization which enables most optimizations. Read more about the options from{' '}
<a
className="link"
rel="noopener noreferrer"
target="_blank"
href="https://clang.llvm.org/docs/CommandGuide/clang.html#cmdoption-o0"
>
clang documentation
</a>
.
</span>
)
const StyledOptimizationText = styled(OptimizationText, {
color: '$mauve12 !important',
fontSize: '200px',
'span a.link': {
color: 'red'
}
})
const CompilerSettings = () => {
const snap = useSnapshot(state)
return (
<Flex css={{ minWidth: 200, flexDirection: 'column', gap: '$5' }}>
<Box>
<Label
style={{
flexDirection: 'row',
display: 'flex'
}}
>
Optimization level{' '}
<Popover
css={{
maxWidth: '240px',
lineHeight: '1.3',
a: {
color: '$purple11'
},
'.dark &': {
backgroundColor: '$black !important',
'.arrow': {
fill: '$colors$black'
}
}
}}
content={<StyledOptimizationText />}
>
<Flex
css={{
position: 'relative',
top: '-1px',
ml: '$1',
backgroundColor: '$mauve8',
borderRadius: '$full',
cursor: 'pointer',
width: '16px',
height: '16px',
alignItems: 'center',
justifyContent: 'center'
}}
>
?
</Flex>
</Popover>
</Label>
<ButtonGroup css={{ mt: '$2', fontFamily: '$monospace' }}>
<Button
css={{ fontFamily: '$monospace' }}
outline={snap.compileOptions.optimizationLevel !== '-O0'}
onClick={() => (state.compileOptions.optimizationLevel = '-O0')}
>
-O0
</Button>
<Button
css={{ fontFamily: '$monospace' }}
outline={snap.compileOptions.optimizationLevel !== '-O1'}
onClick={() => (state.compileOptions.optimizationLevel = '-O1')}
>
-O1
</Button>
<Button
css={{ fontFamily: '$monospace' }}
outline={snap.compileOptions.optimizationLevel !== '-O2'}
onClick={() => (state.compileOptions.optimizationLevel = '-O2')}
>
-O2
</Button>
<Button
css={{ fontFamily: '$monospace' }}
outline={snap.compileOptions.optimizationLevel !== '-O3'}
onClick={() => (state.compileOptions.optimizationLevel = '-O3')}
>
-O3
</Button>
<Button
css={{ fontFamily: '$monospace' }}
outline={snap.compileOptions.optimizationLevel !== '-O4'}
onClick={() => (state.compileOptions.optimizationLevel = '-O4')}
>
-O4
</Button>
<Button
css={{ fontFamily: '$monospace' }}
outline={snap.compileOptions.optimizationLevel !== '-Os'}
onClick={() => (state.compileOptions.optimizationLevel = '-Os')}
>
-Os
</Button>
</ButtonGroup>
</Box>
</Flex>
)
}
const Home: NextPage = () => {
const snap = useSnapshot(state)
const activeFile = snap.files[snap.active] as IFile | undefined
const activeFileExt = getFileExtention(activeFile?.name)
const canCompile = activeFileExt === 'c' || activeFileExt === 'wat'
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 />
{canCompile && (
<Hotkeys
keyName="command+b,ctrl+b"
onKeyDown={() => !snap.compiling && snap.files.length && compileCode(snap.active)}
>
<Flex
css={{
position: 'absolute',
bottom: '$4',
left: '$4',
alignItems: 'center',
display: 'flex',
cursor: 'pointer',
gap: '$2'
}}
>
<Button
variant="primary"
uppercase
disabled={!snap.files.length}
isLoading={snap.compiling}
onClick={() => compileCode(snap.active)}
>
<Play weight="bold" size="16px" />
Compile to Wasm
</Button>
<Popover content={<CompilerSettings />}>
<Button variant="primary" css={{ px: '10px' }}>
<Gear size="16px" />
</Button>
</Popover>
</Flex>
</Hotkeys>
)}
{activeFileExt === 'js' && (
<Hotkeys
keyName="command+b,ctrl+b"
onKeyDown={() => !snap.compiling && snap.files.length && compileCode(snap.active)}
>
<Flex
css={{
position: 'absolute',
bottom: '$4',
left: '$4',
alignItems: 'center',
display: 'flex',
cursor: 'pointer',
gap: '$2'
}}
>
<RunScript file={activeFile as IFile} />
</Flex>
</Hotkeys>
)}
</main>
<Flex css={{ width: '100%' }}>
<Flex
css={{
flex: 1,
background: '$mauve1',
position: 'relative',
borderRight: '1px solid $mauve8'
}}
>
<LogBox title="Development Log" clearLog={() => (state.logs = [])} logs={snap.logs} />
</Flex>
{activeFileExt === 'js' && (
<Flex
css={{
flex: 1
}}
>
<LogBox
Icon={FileJs}
title="Script Log"
logs={snap.scriptLogs}
clearLog={() => (state.scriptLogs = [])}
/>
</Flex>
)}
</Flex>
</Split>
)
}
export default Home

View File

@@ -1,26 +0,0 @@
import type { NextPage } from "next";
import Head from "next/head";
import dynamic from "next/dynamic";
import Footer from "../../components/Footer";
const HooksEditor = dynamic(() => import("../../components/HooksEditor"), {
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;

View File

@@ -1,5 +1,5 @@
const Home = () => {
return <p>homepage</p>;
};
return <p>homepage</p>
}
export default Home;
export default Home

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

@@ -0,0 +1,37 @@
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

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

@@ -0,0 +1,167 @@
import dynamic from 'next/dynamic'
import Split from 'react-split'
import { useSnapshot } from 'valtio'
import { Box, Container, Flex, Tab, Tabs } from '../../components'
import Transaction from '../../components/Transaction'
import state, { renameTxState } from '../../state'
import { getSplit, saveSplit } from '../../state/actions/persistSplits'
import { transactionsState, modifyTxState } from '../../state'
import { useEffect, useState } from 'react'
import { FileJs } from 'phosphor-react'
import RunScript from '../../components/RunScript'
const DebugStream = dynamic(() => import('../../components/DebugStream'), {
ssr: false
})
const LogBox = dynamic(() => import('../../components/LogBox'), {
ssr: false
})
const Accounts = dynamic(() => import('../../components/Accounts'), {
ssr: false
})
const Test = () => {
// This and useEffect is here to prevent useLayoutEffect warnings from react-split
const [showComponent, setShowComponent] = useState(false)
const { transactionLogs } = useSnapshot(state)
const { transactions, activeHeader } = useSnapshot(transactionsState)
const snap = useSnapshot(state)
useEffect(() => {
setShowComponent(true)
}, [])
if (!showComponent) {
return null
}
const hasScripts = Boolean(snap.files.filter(f => f.name.toLowerCase()?.endsWith('.js')).length)
const renderNav = () => (
<Flex css={{ gap: '$3' }}>
{snap.files
.filter(f => f.name.endsWith('.js'))
.map(file => (
<RunScript file={file} key={file.name} />
))}
</Flex>
)
return (
<Container css={{ px: 0 }}>
<Split
direction="vertical"
sizes={
hasScripts && getSplit('testVertical')?.length === 2
? [50, 20, 30]
: hasScripts
? [50, 20, 50]
: [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
label="Transaction"
activeHeader={activeHeader}
// TODO make header a required field
onChangeActive={(idx, header) => {
if (header) transactionsState.activeHeader = header
}}
keepAllAlive
defaultExtension="json"
allowedExtensions={['json']}
onCreateNewTab={header => modifyTxState(header, {})}
onRenameTab={(idx, nwName, oldName = '') => renameTxState(oldName, nwName)}
onCloseTab={(idx, header) => header && modifyTxState(header, undefined)}
>
{transactions.map(({ header, state }) => (
<Tab key={header} header={header}>
<Transaction state={state} header={header} />
</Tab>
))}
</Tabs>
</Box>
<Box css={{ width: '45%', mx: '$2', height: '100%' }}>
<Accounts card hideDeployBtn showHookStats />
</Box>
</Split>
</Flex>
{hasScripts ? (
<Flex
as="div"
css={{
borderTop: '1px solid $mauve6',
background: '$mauve1',
flexDirection: 'column'
}}
>
<LogBox
Icon={FileJs}
title="Helper scripts"
logs={snap.scriptLogs}
clearLog={() => (state.scriptLogs = [])}
renderNav={renderNav}
/>
</Flex>
) : null}
<Flex>
<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

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

@@ -0,0 +1,9 @@
declare module '*.md' {
const content: string
export default content
}
declare module '*.hook-bundle.js' {
const content: string
export default content
}

152
state.ts
View File

@@ -1,152 +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) => {
toast.success('Saved successfully', { position: 'bottom-center' })
}
export const createNewFile = (name: string) => {
state.files.push({ name, language: 'c', content: "" })
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;
toast.success('Compiled successfully!');
console.log(json)
} 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,86 @@
import toast from 'react-hot-toast'
import state, { FaucetAccountRes } from '../index'
import fetchAccountInfo from '../../utils/accountInfo';
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 (name?: string, showToast: boolean = false) => {
if (typeof window === undefined) return
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;
return toast.error(json.error, { id: toastId })
}
const currNames = state.accounts.map(acc => acc.name)
const info = await fetchAccountInfo(json.address, { silent: true })
state.accounts.push({
name: name || names.filter(name => !currNames.includes(name))[0],
xrp: (json.xrp || 0 * 1000000).toString(),
address: json.address,
secret: json.secret,
sequence: info?.Sequence || 1,
hooks: [],
isLoading: false,
version: '2'
})
if (showToast) {
toast.success('New account created', { id: toastId })
}
}
// 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,171 @@
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, activeId)
const file = state.files[activeId]
if (file.name.endsWith('.wat')) {
return compileWat(activeId)
}
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 // TODO Inform user about it.
return
}
// Set loading state to true
state.compiling = true
state.logs = []
try {
file.containsErrors = false
let res: Response
try {
res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
output: 'wasm',
compress: true,
strip: state.compileOptions.strip,
files: [
{
type: 'c',
options: state.compileOptions.optimizationLevel || '-O2',
name: file.name,
src: file.content
}
]
})
})
} catch (error) {
throw Error('Something went wrong, check your network connection and try again!')
}
const json = await res.json()
state.compiling = false
if (!json.success) {
const errors = [json.message]
if (json.tasks && json.tasks.length > 0) {
json.tasks.forEach((task: any) => {
if (!task.success) {
errors.push(task?.console)
}
})
}
throw errors
}
try {
// Decode base64 encoded wasm that is coming back from the endpoint
const bufferData = await decodeBinary(json.output)
// Import wabt from and create human readable version of wasm file and
// put it into state
const ww = await (await import('wabt')).default()
const myModule = ww.readWasm(new Uint8Array(bufferData), {
readDebugNames: true
})
myModule.applyNames()
const wast = myModule.toText({ foldExprs: false, inlineExport: false })
file.compiledContent = ref(bufferData)
file.lastCompiled = new Date()
file.compiledValueSnapshot = file.content
file.compiledWatContent = wast
} catch (error) {
throw Error('Invalid compilation result produced, check your code for errors and try again!')
}
toast.success('Compiled successfully!', { 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'
})
} catch (err) {
console.log(err)
if (err instanceof Array && typeof err[0] === 'string') {
err.forEach(message => {
state.logs.push({
type: 'error',
message
})
})
} else if (err instanceof Error) {
state.logs.push({
type: 'error',
message: err.message
})
} else {
state.logs.push({
type: 'error',
message: 'Something went wrong, come back later!'
})
}
state.compiling = false
toast.error(`Error occurred while compiling!`, { position: 'bottom-center' })
file.containsErrors = true
}
}
export const compileWat = async (activeId: number) => {
if (state.compiling) return;
const file = state.files[activeId]
state.compiling = true
state.logs = []
try {
const wabt = await (await import('wabt')).default()
const module = wabt.parseWat(file.name, file.content);
module.resolveNames();
module.validate();
const { buffer } = module.toBinary({
log: false,
write_debug_names: true,
});
file.compiledContent = ref(buffer)
file.lastCompiled = new Date()
file.compiledValueSnapshot = file.content
file.compiledWatContent = file.content
toast.success('Compiled successfully!', { 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'
})
} catch (err) {
console.log(err)
let message = "Error compiling WAT file!"
if (err instanceof Error) {
message = err.message
}
state.logs.push({
type: 'error',
message
})
toast.error(`Error occurred while compiling!`, { position: 'bottom-center' })
file.containsErrors = true
}
state.compiling = false
}

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