Compare commits

...

317 Commits

Author SHA1 Message Date
Denis Angell
41d9a399d7 remove LastLedgerSequence 2025-02-28 10:19:47 +01:00
Ekiserrepé
4c6a0b5a40 Bugs and Discussions button was broken, new link! (#318) 2025-02-28 10:10:59 +01:00
Tristan
0616149b6c New xahau share image 2025-02-18 09:45:56 +01:00
tequ
ee86d657c9 bump next-auth, typescript (#314) 2025-02-13 14:43:21 +01:00
tequ
12ebd4c1bc Fixed to retrieve the template header file from gist (#316) 2025-02-13 14:32:17 +01:00
Wietse Wind
6ed0b54fc5 Merge pull request #310 from Ekiserrepe/main
Readme and xah value updated
2025-01-02 01:21:03 +01:00
Ekiserrepé
b7e1343055 Github link update 2025-01-01 23:29:04 +01:00
Ekiserrepé
a13dbaeb53 Merge branch 'Xahau:main' into main 2025-01-01 23:14:23 +01:00
Ekiserrepé
98a7a1ee72 Readme updated 2025-01-01 22:22:26 +01:00
Ekiserrepé
46c0ec9682 value xah updated 2025-01-01 22:20:33 +01:00
Wietse Wind
fbc27b58c3 Merge pull request #309 from Ekiserrepe/main
Xahau translation
2025-01-01 22:07:02 +01:00
Ekiserrepé
635e1e22aa Update XAH capitals 2025-01-01 21:29:35 +01:00
Ekiserrepé
62ca0603fc Xahau translation 2025-01-01 18:03:13 +01:00
Wietse Wind
3234ba851a Merge pull request #308 from XRPLF/hooks-xahau-endpoints
update explorer & wss network URLS @ env sample
2023-10-30 23:06:27 +01:00
Wietse Wind
26aeea5a0d update explorer & wss network URLS @ env sample 2023-10-30 23:04:53 +01:00
Wietse Wind
4a1bd98bd4 Merge pull request #306 from Transia-RnD/main
Fix Bad URIToken Amount
2023-08-01 05:32:33 +02:00
Denis Angell
4171e618a2 add uritoken flag 2023-08-01 05:31:02 +02:00
Denis Angell
1860b2d9ce fix bad amount 2023-08-01 05:23:25 +02:00
Denis Angell
488828f9d6 remove nft transactions 2023-08-01 05:23:09 +02:00
Wietse Wind
9f2105f6d3 Merge pull request #305 from Transia-RnD/main
Add URIToken features
2023-08-01 05:11:58 +02:00
Denis Angell
6beccaef68 update ripple-binary-codec 2023-06-19 16:55:25 +00:00
Denis Angell
215af7258c Merge branch 'main' into main 2023-06-15 07:04:13 +00:00
Denis Angell
ac3137088b remove default hookon 2023-06-15 07:01:15 +00:00
Denis Angell
1b8debda87 bump ripple-binary-codec & update hook on 2023-06-15 06:35:26 +00:00
Wietse Wind
c50c7a5860 Merge pull request #304 from Transia-RnD/main
Update readme link
2023-06-11 08:34:34 +02:00
Denis Angell
d21cda21d0 !fixup 2023-06-06 12:53:43 +00:00
Denis Angell
1cae0f161e change readme link 2023-06-06 12:28:19 +00:00
muzamil
412e3f2bbf Merge pull request #303 from XRPLF/fix/hex-value
Take tx memo data and parameter value as hex.
2023-03-29 17:43:11 +05:30
muzam1l
dc37b1911a Take tx memo data and parameter value as hex. 2023-03-29 15:04:14 +05:30
muzamil
c348868c89 Merge pull request #302 from XRPLF/fix/hook-on
Fix HookOn initial value.
2023-03-28 16:28:35 +05:30
muzam1l
2c3cfebe3a Fix HookOn initial value. 2023-03-28 15:14:43 +05:30
muzamil
6265a9cdbf Merge pull request #300 from XRPLF/fix/to-hex
Fix hex logic.
2023-03-27 21:20:08 +05:30
muzamil
1321b498cf Merge pull request #299 from XRPLF/fix/invoke-tx
Add `Destination` field to Invoke transaction.
2023-03-27 21:18:35 +05:30
muzam1l
801d9778cb Fix hex logic. 2023-03-27 19:21:14 +05:30
muzam1l
2cf18ef61c Add Destination field to Invoke transaction. 2023-03-27 15:32:25 +05:30
muzamil
4d2dc16ce5 Merge pull request #296 from XRPLF/feat/account-ui
Extend transactions Account UI.
2023-03-23 20:45:27 +05:30
muzam1l
9ecf5478e6 Remove tx Destination default values. 2023-03-23 19:39:56 +05:30
muzam1l
6d1ef110b7 Token Issuer account UI. 2023-03-23 17:52:17 +05:30
muzam1l
b9edfcd63b Fix: nextjs build. 2023-03-23 16:55:32 +05:30
muzam1l
b653d9a9cb Fix: Remove last traces of custom Destination handling! 2023-03-23 16:47:25 +05:30
muzam1l
da28e0a7d1 Use "creatable select" for accounts. 2023-03-23 16:45:24 +05:30
muzam1l
8a5b83d57f Fix: Don't remove fields in JSON mode if empty. 2023-03-23 16:26:29 +05:30
muzam1l
025eff6cf2 Add Select UI for account. 2023-03-23 16:12:35 +05:30
muzam1l
62d521b2cc Add owner field to NFTokenCreateOffer. 2023-03-23 15:51:36 +05:30
muzam1l
7aafca21df Remove Destination as special field. 2023-03-23 15:49:17 +05:30
muzamil
80f58e903c Merge pull request #294 from XRPLF/feat/amount-ui
Transaction amount UI.
2023-03-20 14:04:30 +05:30
muzam1l
c4af3df017 Add DeliverMin field to tx CheckCash. 2023-03-17 20:10:57 +05:30
muzam1l
5d8d142bc4 Catch all estimate fee errors. 2023-03-17 19:01:32 +05:30
muzam1l
e27a71d713 Estimate fee correct error message and amount input type. 2023-03-17 18:57:03 +05:30
muzam1l
e08b07cbeb Update tx OfferCreate. 2023-03-17 18:03:37 +05:30
muzam1l
e4936c03ef Css changes. 2023-03-17 17:17:29 +05:30
muzam1l
21a69ac8ea minor type fix. 2023-03-17 16:59:12 +05:30
muzam1l
52e4f219f7 Transaction amount UI. 2023-03-17 16:49:00 +05:30
muzamil
e1f34c4beb Merge pull request #291 from XRPLF/fix/sequence-ui
Fix Account sequence UI reset.
2023-03-14 21:30:35 +05:30
muzam1l
54a89c969e Fix account sequence reset. 2023-03-14 15:38:09 +05:30
muzamil
ded867d997 Merge pull request #289 from XRPLF/fix/tx
Account sequence.
2023-03-10 17:31:55 +05:30
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
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
174 changed files with 10487 additions and 20433 deletions

View File

@@ -1,10 +1,12 @@
NEXTAUTH_URL=https://example.com NEXTAUTH_URL=https://example.com
NEXTAUTH_SECRET="1234"
GITHUB_SECRET="" GITHUB_SECRET=""
GITHUB_ID="" GITHUB_ID=""
NEXT_PUBLIC_COMPILE_API_ENDPOINT="http://localhost:9000/api/build" NEXT_PUBLIC_COMPILE_API_ENDPOINT="http://localhost:9000/api/build"
NEXT_PUBLIC_COMPILE_API_BASE_URL="http://localhost:9000" NEXT_PUBLIC_COMPILE_API_BASE_URL="http://localhost:9000"
NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT="ws://localhost:9000/language-server/c" NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT="ws://localhost:9000/language-server/c"
NEXT_PUBLIC_TESTNET_URL="hooks-testnet-v2.xrpl-labs.com" NEXT_PUBLIC_TESTNET_URL="xahau-test.net"
NEXT_PUBLIC_DEBUG_STREAM_URL="hooks-testnet-v2-debugstream.xrpl-labs.com" NEXT_PUBLIC_DEBUG_STREAM_URL="xahau-test.net/debugstream"
NEXT_PUBLIC_EXPLORER_URL="hooks-testnet-v2-explorer.xrpl-labs.com" NEXT_PUBLIC_EXPLORER_URL="explorer.xahau-test.net"
NEXT_PUBLIC_SITE_URL=http://localhost:3000 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
.vercel .vercel
.vscode
# yarn
.yarnrc.yml
.yarn/

View File

@@ -1 +1,38 @@
*.md 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 # Xahau Hooks Builder
This is the repository for XRPL Hooks IDE. This project is built with Next.JS https://builder.xahau.network/
This is the repository for Xahau Hooks Builder. This project is built with Next.JS
## General ## General
@@ -106,3 +108,5 @@ To learn more about Next.js, take a look at the following resources:
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

View File

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

View File

@@ -1,65 +1,62 @@
import { FC, ReactNode } from "react"; import { FC, ReactNode } from 'react'
import { proxy, useSnapshot } from "valtio"; import { proxy, useSnapshot } from 'valtio'
import Button from "../Button"; import Button from '../Button'
import Flex from "../Flex"; import Flex from '../Flex'
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
AlertDialogCancel, AlertDialogCancel,
AlertDialogContent, AlertDialogContent,
AlertDialogDescription, AlertDialogDescription,
AlertDialogTitle, AlertDialogTitle
} from "./primitive"; } from './primitive'
export interface AlertState { export interface AlertState {
isOpen: boolean; isOpen: boolean
title?: string; title?: string
body?: ReactNode; body?: ReactNode
cancelText?: string; cancelText?: string
confirmText?: string; confirmText?: string
confirmPrefix?: ReactNode; confirmPrefix?: ReactNode
onConfirm?: () => any; onConfirm?: () => any
onCancel?: () => any; onCancel?: () => any
} }
export const alertState = proxy<AlertState>({ export const alertState = proxy<AlertState>({
isOpen: false, isOpen: false
}); })
const Alert: FC = () => { const Alert: FC = () => {
const { const {
title = "Are you sure?", title = 'Are you sure?',
isOpen, isOpen,
body, body,
cancelText, cancelText,
confirmText = "Ok", confirmText = 'Ok',
confirmPrefix, confirmPrefix,
onCancel, onCancel,
onConfirm, onConfirm
} = useSnapshot(alertState); } = useSnapshot(alertState)
return ( return (
<AlertDialog <AlertDialog open={isOpen} onOpenChange={value => (alertState.isOpen = value)}>
open={isOpen}
onOpenChange={value => (alertState.isOpen = value)}
>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogTitle>{title}</AlertDialogTitle> <AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{body}</AlertDialogDescription> <AlertDialogDescription>{body}</AlertDialogDescription>
<Flex css={{ justifyContent: "flex-end", gap: "$3" }}> <Flex css={{ justifyContent: 'flex-end', gap: '$3' }}>
{(cancelText || onCancel) && ( {(cancelText || onCancel) && (
<AlertDialogCancel asChild> <AlertDialogCancel asChild>
<Button css={{ minWidth: "$16" }} outline onClick={onCancel}> <Button css={{ minWidth: '$16' }} outline onClick={onCancel}>
{cancelText || "Cancel"} {cancelText || 'Cancel'}
</Button> </Button>
</AlertDialogCancel> </AlertDialogCancel>
)} )}
<AlertDialogAction asChild> <AlertDialogAction asChild>
<Button <Button
css={{ minWidth: "$16" }} css={{ minWidth: '$16' }}
variant="primary" variant="primary"
onClick={async () => { onClick={async () => {
await onConfirm?.(); await onConfirm?.()
alertState.isOpen = false; alertState.isOpen = false
}} }}
> >
{confirmPrefix} {confirmPrefix}
@@ -69,7 +66,7 @@ const Alert: FC = () => {
</Flex> </Flex>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
); )
}; }
export default Alert; export default Alert

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
import { styled } from "../stitches.config"; import { styled } from '../stitches.config'
import Box from "./Box"; import Box from './Box'
const Container = styled(Box, { const Container = styled(Box, {
width: "100%", width: '100%',
marginLeft: "auto", marginLeft: 'auto',
marginRight: "auto", marginRight: 'auto',
px: "$4", px: '$4',
maxWidth: "100%", 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

View File

@@ -1,38 +1,119 @@
import { useCallback, useEffect } from "react"; import { useEffect } from 'react'
import { proxy, ref, useSnapshot } from "valtio"; import ReconnectingWebSocket, { CloseEvent } from 'reconnecting-websocket'
import { Select } from "."; import { proxy, ref, useSnapshot } from 'valtio'
import state, { ILog, transactionsState } from "../state"; import { subscribeKey } from 'valtio/utils'
import { extractJSON } from "../utils/json"; import { Select } from '.'
import LogBox from "./LogBox"; import state, { ILog, transactionsState } from '../state'
import { extractJSON } from '../utils/json'
import EnrichLog from './EnrichLog'
import LogBox from './LogBox'
interface ISelect<T = string> { interface ISelect<T = string> {
label: string; label: string
value: T; value: T
} }
export interface IStreamState { export interface IStreamState {
selectedAccount: ISelect | null; selectedAccount: ISelect | null
status: "idle" | "opened" | "closed"; status: 'idle' | 'opened' | 'closed'
statusChangeTimestamp?: number; statusChangeTimestamp?: number
logs: ILog[]; logs: ILog[]
socket?: WebSocket; socket?: ReconnectingWebSocket
} }
export const streamState = proxy<IStreamState>({ export const streamState = proxy<IStreamState>({
selectedAccount: null as ISelect | null, selectedAccount: null as ISelect | null,
status: "idle", status: 'idle',
logs: [] as ILog[], 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 DebugStream = () => {
const { selectedAccount, logs, socket } = useSnapshot(streamState); const { selectedAccount, logs } = useSnapshot(streamState)
const { activeHeader: activeTxTab } = useSnapshot(transactionsState); const { activeHeader: activeTxTab } = useSnapshot(transactionsState)
const { accounts } = useSnapshot(state); const { accounts } = useSnapshot(state)
const accountOptions = accounts.map(acc => ({ const accountOptions = accounts.map(acc => ({
label: acc.name, label: acc.name,
value: acc.address, value: acc.address
})); }))
const renderNav = () => ( const renderNav = () => (
<> <>
@@ -42,176 +123,60 @@ const DebugStream = () => {
options={accountOptions} options={accountOptions}
hideSelectedOptions hideSelectedOptions
value={selectedAccount} value={selectedAccount}
onChange={acc => (streamState.selectedAccount = acc as any)} onChange={acc => {
css={{ width: "100%" }} streamState.socket?.close(4999, 'Old connection closed because user switched account')
streamState.selectedAccount = acc as any
}}
css={{ width: '100%' }}
/> />
</> </>
); )
useEffect(() => { useEffect(() => {
const account = selectedAccount?.value; const account = transactionsState.transactions.find(tx => tx.header === activeTxTab)?.state
if (account && (!socket || !socket.url.endsWith(account))) { .selectedAccount
socket?.close();
streamState.socket = ref(
new WebSocket(
`wss://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/${account}`
)
);
} else if (!account && socket) {
socket.close();
streamState.socket = undefined;
}
}, [selectedAccount?.value, socket]);
const onMount = useCallback(async () => {
// deliberately using `proxy` values and not the `useSnapshot` ones to have no dep list
const acc = streamState.selectedAccount;
const status = streamState.status;
if (status === "opened" && acc) {
// fetch the missing ones
try {
const url = `https://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/recent/${acc?.value}`;
// TODO Remove after api sets cors properly
const res = await fetch("/api/proxy", {
method: "POST",
body: JSON.stringify({ url }),
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) return;
const body = await res.json();
if (!body?.logs) return;
const start = streamState.statusChangeTimestamp || 0;
streamState.logs = [];
pushLog(`Debug stream opened for account ${acc.value}`, {
type: "success",
});
const logs = Object.entries(body.logs).filter(([tm]) => +tm >= start);
logs.forEach(([tm, log]) => pushLog(log));
} catch (error) {
console.error(error);
}
}
}, []);
useEffect(() => {
onMount();
}, [onMount]);
useEffect(() => {
const account = selectedAccount?.value;
const socket = streamState.socket;
if (!socket) return;
const onOpen = () => {
streamState.logs = [];
streamState.status = "opened";
streamState.statusChangeTimestamp = Date.now();
pushLog(`Debug stream opened for account ${account}`, {
type: "success",
});
};
const onError = () => {
pushLog("Something went wrong! Check your connection and try again.", {
type: "error",
});
};
const onClose = (e: CloseEvent) => {
pushLog(`Connection was closed. [code: ${e.code}]`, {
type: "error",
});
streamState.selectedAccount = null;
streamState.status = "closed";
streamState.statusChangeTimestamp = Date.now();
};
const onMessage = (event: any) => {
pushLog(event.data);
};
socket.addEventListener("open", onOpen);
socket.addEventListener("close", onClose);
socket.addEventListener("error", onError);
socket.addEventListener("message", onMessage);
return () => {
socket.removeEventListener("open", onOpen);
socket.removeEventListener("close", onClose);
socket.removeEventListener("message", onMessage);
socket.removeEventListener("error", onError);
};
}, [selectedAccount?.value, socket]);
useEffect(() => {
const account = transactionsState.transactions.find(
tx => tx.header === activeTxTab
)?.state.selectedAccount;
if (account && account.value !== streamState.selectedAccount?.value) if (account && account.value !== streamState.selectedAccount?.value)
streamState.selectedAccount = account; streamState.selectedAccount = account
}, [activeTxTab]); }, [activeTxTab])
const clearLog = () => {
streamState.logs = [];
streamState.statusChangeTimestamp = Date.now();
};
return ( return (
<LogBox <LogBox enhanced renderNav={renderNav} title="Debug stream" logs={logs} clearLog={clearLog} />
enhanced )
renderNav={renderNav} }
title="Debug stream"
logs={logs}
clearLog={clearLog}
/>
);
};
export default DebugStream; export default DebugStream
export const pushLog = ( export const pushLog = (str: any, opts: Partial<Pick<ILog, 'type'>> = {}): ILog | undefined => {
str: any, if (!str) return
opts: Partial<Pick<ILog, "type">> = {} if (typeof str !== 'string') throw Error('Unrecognized debug log stream!')
): 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 match = str.match(/([\s\S]+(?:UTC|ISO|GMT[+|-]\d+))?\ ?([\s\S]*)/m)
const [_, tm, msg] = match || []; const [_, tm, msg] = match || []
const timestamp = Date.parse(tm || "") || undefined; const timestamp = Date.parse(tm || '') || undefined
const timestring = !timestamp ? tm : new Date(timestamp).toLocaleTimeString(); const timestring = !timestamp ? tm : new Date(timestamp).toLocaleTimeString()
const extracted = extractJSON(msg); const extracted = extractJSON(msg)
const message = !extracted const _message = !extracted ? msg : msg.slice(0, extracted.start) + msg.slice(extracted.end + 1)
? msg const message = ref(<EnrichLog str={_message} />)
: msg.slice(0, extracted.start) + msg.slice(extracted.end + 1);
const jsonData = extracted const _jsonData = extracted ? JSON.stringify(extracted.result, null, 2) : undefined
? JSON.stringify(extracted.result, null, 2) const jsonData = _jsonData ? ref(<EnrichLog str={_jsonData} />) : undefined
: undefined;
if (extracted?.result?.id?._Request?.includes("hooks-builder-req")) { if (extracted?.result?.id?._Request?.includes('hooks-builder-req')) {
return; return
} }
const { type = "log" } = opts; const { type = 'log' } = opts
const log: ILog = { const log: ILog = {
type, type,
message, message,
timestring, timestring,
jsonData, jsonData,
defaultCollapsed: true, defaultCollapsed: true
}; }
if (log) streamState.logs.push(log); if (log) streamState.logs.push(log)
return log; return log
}; }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useRef, ReactNode } from 'react'
import { import {
Plus,
Share, Share,
DownloadSimple, DownloadSimple,
Gear, Gear,
@@ -11,296 +10,151 @@ import {
CloudArrowUp, CloudArrowUp,
CaretDown, CaretDown,
User, User,
FilePlus, FilePlus
} from "phosphor-react"; } from 'phosphor-react'
import Image from "next/image"; import Image from 'next/image'
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuArrow, DropdownMenuArrow,
DropdownMenuSeparator, DropdownMenuSeparator
} from "./DropdownMenu"; } from './DropdownMenu'
import NewWindow from "react-new-window"; import NewWindow from 'react-new-window'
import { signOut, useSession } from "next-auth/react"; import { signOut, useSession } from 'next-auth/react'
import { useSnapshot } from "valtio"; import { useSnapshot } from 'valtio'
import toast from "react-hot-toast"; import toast from 'react-hot-toast'
import { import { syncToGist, updateEditorSettings, downloadAsZip } from '../state/actions'
createNewFile, import state from '../state'
syncToGist, import Box from './Box'
updateEditorSettings, import Button from './Button'
downloadAsZip, import Container from './Container'
} from "../state/actions";
import state from "../state";
import Box from "./Box";
import Button from "./Button";
import Container from "./Container";
import { import {
Dialog, Dialog,
DialogTrigger, DialogTrigger,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
DialogDescription, DialogDescription,
DialogClose, DialogClose
} from "./Dialog"; } from './Dialog'
import Flex from "./Flex"; import Flex from './Flex'
import Stack from "./Stack"; import Stack from './Stack'
import { Input, Label } from "./Input"; import { Input, Label } from './Input'
import Text from "./Text"; import Tooltip from './Tooltip'
import Tooltip from "./Tooltip"; import { showAlert } from '../state/actions/showAlert'
import { styled } from "../stitches.config";
import { showAlert } from "../state/actions/showAlert";
const ErrorText = styled(Text, { const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
color: "$error", const snap = useSnapshot(state)
mt: "$1", const [editorSettingsOpen, setEditorSettingsOpen] = useState(false)
display: "block", const { data: session, status } = useSession()
}); const [popup, setPopUp] = useState(false)
const [editorSettings, setEditorSettings] = useState(snap.editorSettings)
const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
const snap = useSnapshot(state);
const [editorSettingsOpen, setEditorSettingsOpen] = useState(false);
const [isNewfileDialogOpen, setIsNewfileDialogOpen] = useState(false);
const [newfileError, setNewfileError] = useState<string | null>(null);
const [filename, setFilename] = useState("");
const { data: session, status } = useSession();
const [popup, setPopUp] = useState(false);
const [editorSettings, setEditorSettings] = useState(snap.editorSettings);
useEffect(() => { useEffect(() => {
if (session && session.user && popup) { if (session && session.user && popup) {
setPopUp(false); setPopUp(false)
} }
}, [session, popup]); }, [session, popup])
// when filename changes, reset error
useEffect(() => {
setNewfileError(null);
}, [filename, setNewfileError]);
const showNewGistAlert = () => { const showNewGistAlert = () => {
showAlert("Are you sure?", { showAlert('Are you sure?', {
body: ( body: (
<> <>
This action will create new <strong>public</strong> Github Gist from This action will create new <strong>public</strong> Github Gist from your current saved
your current saved files. You can delete gist anytime from your GitHub files. You can delete gist anytime from your GitHub Gists page.
Gists page.
</> </>
), ),
cancelText: "Cancel", cancelText: 'Cancel',
confirmText: "Create new Gist", confirmText: 'Create new Gist',
confirmPrefix: <FilePlus size="15px" />, confirmPrefix: <FilePlus size="15px" />,
onConfirm: () => syncToGist(session, true), onConfirm: () => syncToGist(session, true)
}); })
}; }
const validateFilename = useCallback( const scrollRef = useRef<HTMLDivElement>(null)
(filename: string): { error: string | null } => { const containerRef = useRef<HTMLDivElement>(null)
// check if filename already exists
if (!filename) {
return { error: "You need to add filename" };
}
if (snap.files.find(file => file.name === filename)) {
return { error: "Filename already exists." };
}
if (!filename.includes(".") || filename[filename.length - 1] === ".") {
return { error: "Filename should include file extension" };
}
// check for illegal characters
const ALPHA_NUMERICAL_REGEX = /^[A-Za-z0-9_-]+[.][A-Za-z0-9]{1,4}$/g;
if (!filename.match(ALPHA_NUMERICAL_REGEX)) {
return {
error: `Filename can contain only characters from a-z, A-Z, 0-9, "_" and "-" and it needs to have file extension (e.g. ".c")`,
};
}
return { error: null };
},
[snap.files]
);
const handleConfirm = useCallback(() => {
// add default extension in case omitted
const chk = validateFilename(filename);
if (chk && chk.error) {
setNewfileError(`Error: ${chk.error}`);
return;
}
setIsNewfileDialogOpen(false);
createNewFile(filename);
setFilename("");
}, [filename, setIsNewfileDialogOpen, setFilename, validateFilename]);
const files = snap.files;
return ( return (
<Flex css={{ flexShrink: 0, gap: "$0" }}> <Flex css={{ flexShrink: 0, gap: '$0' }}>
<Flex <Flex
id="kissa"
ref={scrollRef}
css={{ css={{
overflowX: "scroll", overflowX: 'scroll',
py: "$3", overflowY: 'hidden',
py: '$3',
pb: '$0',
flex: 1, flex: 1,
"&::-webkit-scrollbar": { '&::-webkit-scrollbar': {
height: 0, height: '0.3em',
background: "transparent", 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 }}> <Container css={{ flex: 1 }} ref={containerRef}>
<Stack {renderNav?.()}
css={{
gap: "$3",
flex: 1,
flexWrap: "nowrap",
marginBottom: "-1px",
}}
>
{files &&
files.length > 0 &&
files.map((file, index) => {
if (!file.compiledContent && showWat) {
return null;
}
return (
<Button
size="sm"
outline={
showWat ? snap.activeWat !== index : snap.active !== index
}
onClick={() => (state.active = index)}
key={file.name + index}
css={{
"&:hover": {
span: {
visibility: "visible",
},
},
}}
>
{file.name}
{showWat && ".wat"}
{!showWat && (
<Box
as="span"
css={{
display: "flex",
p: "2px",
borderRadius: "$full",
mr: "-4px",
"&:hover": {
// boxSizing: "0px 0px 1px",
backgroundColor: "$mauve2",
color: "$mauve12",
},
}}
onClick={(ev: React.MouseEvent<HTMLElement>) => {
ev.stopPropagation();
// Remove file from state
state.files.splice(index, 1);
// Change active file state
// If deleted file is behind active tab
// we keep the current state otherwise
// select previous file on the list
state.active =
index > snap.active ? snap.active : snap.active - 1;
}}
>
<X size="9px" weight="bold" />
</Box>
)}
</Button>
);
})}
{!showWat && (
<Dialog
open={isNewfileDialogOpen}
onOpenChange={setIsNewfileDialogOpen}
>
<DialogTrigger asChild>
<Button
ghost
size="sm"
css={{ alignItems: "center", px: "$2", mr: "$3" }}
>
<Plus size="16px" />{" "}
{snap.files.length === 0 && "Add new file"}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Create new file</DialogTitle>
<DialogDescription>
<Label>Filename</Label>
<Input
value={filename}
onChange={e => setFilename(e.target.value)}
onKeyPress={e => {
if (e.key === "Enter") {
handleConfirm();
}
}}
/>
<ErrorText>{newfileError}</ErrorText>
</DialogDescription>
<Flex
css={{
marginTop: 25,
justifyContent: "flex-end",
gap: "$3",
}}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<Button variant="primary" onClick={handleConfirm}>
Create file
</Button>
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
)}
</Stack>
</Container> </Container>
</Flex> </Flex>
<Flex <Flex
css={{ css={{
py: "$3", py: '$3',
backgroundColor: "$mauve2", backgroundColor: '$mauve2',
zIndex: 1, zIndex: 1
}} }}
> >
<Container <Container css={{ width: 'unset', display: 'flex', alignItems: 'center' }}>
css={{ width: "unset", display: "flex", alignItems: "center" }} {status === 'authenticated' ? (
>
{status === "authenticated" ? (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Box <Box
css={{ css={{
display: "flex", display: 'flex',
borderRadius: "$full", borderRadius: '$full',
overflow: "hidden", overflow: 'hidden',
width: "$6", width: '$6',
height: "$6", height: '$6',
boxShadow: "0px 0px 0px 1px $colors$mauve11", boxShadow: '0px 0px 0px 1px $colors$mauve11',
position: "relative", position: 'relative',
mr: "$3", mr: '$3',
"@hover": { '@hover': {
"&:hover": { '&:hover': {
cursor: "pointer", cursor: 'pointer',
boxShadow: "0px 0px 0px 1px $colors$mauve12", boxShadow: '0px 0px 0px 1px $colors$mauve12'
}, }
}, }
}} }}
> >
<Image <Image
src={session?.user?.image || ""} src={session?.user?.image || ''}
width="30px" width="30px"
height="30px" height="30px"
objectFit="cover" objectFit="cover"
@@ -310,21 +164,16 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem disabled onClick={() => signOut()}> <DropdownMenuItem disabled onClick={() => signOut()}>
<User size="16px" /> {session?.user?.username} ( <User size="16px" /> {session?.user?.username} ({session?.user.name})
{session?.user.name})
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onClick={() => window.open(`http://gist.github.com/${session?.user.username}`)}
window.open(
`http://gist.github.com/${session?.user.username}`
)
}
> >
<ArrowSquareOut size="16px" /> <ArrowSquareOut size="16px" />
Go to your Gist Go to your Gist
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={() => signOut({ callbackUrl: "/" })}> <DropdownMenuItem onClick={() => signOut({ callbackUrl: '/' })}>
<SignOut size="16px" /> Log out <SignOut size="16px" /> Log out
</DropdownMenuItem> </DropdownMenuItem>
@@ -332,48 +181,43 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
) : ( ) : (
<Button <Button outline size="sm" css={{ mr: '$3' }} onClick={() => setPopUp(true)}>
outline
size="sm"
css={{ mr: "$3" }}
onClick={() => setPopUp(true)}
>
<GithubLogo size="16px" /> Login <GithubLogo size="16px" /> Login
</Button> </Button>
)} )}
<Stack <Stack
css={{ css={{
display: "inline-flex", display: 'inline-flex',
marginLeft: "auto", marginLeft: 'auto',
flexShrink: 0, flexShrink: 0,
gap: "$0", gap: '$0',
borderRadius: "$sm", borderRadius: '$sm',
boxShadow: "inset 0px 0px 0px 1px $colors$mauve10", boxShadow: 'inset 0px 0px 0px 1px $colors$mauve10',
zIndex: 9, zIndex: 9,
position: "relative", position: 'relative',
button: { button: {
borderRadius: 0, borderRadius: 0,
px: "$2", px: '$2',
alignSelf: "flex-start", alignSelf: 'flex-start',
boxShadow: "none", boxShadow: 'none'
}, },
"button:not(:first-child):not(:last-child)": { 'button:not(:first-child):not(:last-child)': {
borderRight: 0, borderRight: 0,
borderLeft: 0, borderLeft: 0
}, },
"button:first-child": { 'button:first-child': {
borderTopLeftRadius: "$sm", borderTopLeftRadius: '$sm',
borderBottomLeftRadius: "$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:last-child': {
borderTopRightRadius: '$sm',
borderBottomRightRadius: '$sm',
boxShadow: 'inset 0px 0px 0px 1px $colors$mauve10',
'&:hover': {
boxShadow: 'inset 0px 0px 0px 1px $colors$mauve12'
}
}
}} }}
> >
<Tooltip content="Download as ZIP"> <Tooltip content="Download as ZIP">
@@ -382,7 +226,7 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
onClick={downloadAsZip} onClick={downloadAsZip}
outline outline
size="sm" size="sm"
css={{ alignItems: "center" }} css={{ alignItems: 'center' }}
> >
<DownloadSimple size="16px" /> <DownloadSimple size="16px" />
</Button> </Button>
@@ -391,12 +235,10 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
<Button <Button
outline outline
size="sm" size="sm"
css={{ alignItems: "center" }} css={{ alignItems: 'center' }}
onClick={() => { onClick={() => {
navigator.clipboard.writeText( navigator.clipboard.writeText(`${window.location.origin}/develop/${snap.gistId}`)
`${window.location.origin}/develop/${snap.gistId}` toast.success('Copied share link to clipboard!')
);
toast.success("Copied share link to clipboard!");
}} }}
> >
<Share size="16px" /> <Share size="16px" />
@@ -406,9 +248,9 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
content={ content={
session && session.user session && session.user
? snap.gistOwner === session?.user.username ? snap.gistOwner === session?.user.username
? "Sync to Gist" ? 'Sync to Gist'
: "Create as a new Gist" : 'Create as a new Gist'
: "You need to be logged in to sync with Gist" : 'You need to be logged in to sync with Gist'
} }
> >
<Button <Button
@@ -416,15 +258,15 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
size="sm" size="sm"
isDisabled={!session || !session.user} isDisabled={!session || !session.user}
isLoading={snap.gistLoading} isLoading={snap.gistLoading}
css={{ alignItems: "center" }} css={{ alignItems: 'center' }}
onClick={() => { onClick={() => {
if (!session || !session.user) { if (!session || !session.user) {
return; return
} }
if (snap.gistOwner === session?.user.username) { if (snap.gistOwner === session?.user.username) {
syncToGist(session); syncToGist(session)
} else { } else {
showNewGistAlert(); showNewGistAlert()
} }
}} }}
> >
@@ -443,38 +285,33 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem <DropdownMenuItem disabled={snap.zipLoading} onClick={downloadAsZip}>
disabled={snap.zipLoading}
onClick={downloadAsZip}
>
<DownloadSimple size="16px" /> Download as ZIP <DownloadSimple size="16px" /> Download as ZIP
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
navigator.clipboard.writeText( navigator.clipboard.writeText(
`${window.location.origin}/develop/${snap.gistId}` `${window.location.origin}/develop/${snap.gistId}`
); )
toast.success("Copied share link to clipboard!"); toast.success('Copied share link to clipboard!')
}} }}
> >
<Share size="16px" /> <Share size="16px" />
Copy share link to clipboard Copy share link to clipboard
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
disabled={ disabled={session?.user.username !== snap.gistOwner || !snap.gistId}
session?.user.username !== snap.gistOwner || !snap.gistId
}
onClick={() => { onClick={() => {
syncToGist(session); syncToGist(session)
}} }}
> >
<CloudArrowUp size="16px" /> Push to Gist <CloudArrowUp size="16px" /> Push to Gist
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
disabled={status !== "authenticated"} disabled={status !== 'authenticated'}
onClick={() => { onClick={() => {
showNewGistAlert(); showNewGistAlert()
}} }}
> >
<FilePlus size="16px" /> Create as a new Gist <FilePlus size="16px" /> Create as a new Gist
@@ -489,9 +326,7 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
</DropdownMenu> </DropdownMenu>
</Stack> </Stack>
{popup && !session ? ( {popup && !session ? <NewWindow center="parent" url="/sign-in" /> : null}
<NewWindow center="parent" url="/sign-in" />
) : null}
</Container> </Container>
</Flex> </Flex>
@@ -512,39 +347,33 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
onChange={e => onChange={e =>
setEditorSettings(curr => ({ setEditorSettings(curr => ({
...curr, ...curr,
tabSize: Number(e.target.value), tabSize: Number(e.target.value)
})) }))
} }
/> />
</DialogDescription> </DialogDescription>
<Flex css={{ marginTop: 25, justifyContent: "flex-end", gap: "$3" }}> <Flex css={{ marginTop: 25, justifyContent: 'flex-end', gap: '$3' }}>
<DialogClose asChild> <DialogClose asChild>
<Button <Button outline onClick={() => updateEditorSettings(editorSettings)}>
outline
onClick={() => updateEditorSettings(editorSettings)}
>
Cancel Cancel
</Button> </Button>
</DialogClose> </DialogClose>
<DialogClose asChild> <DialogClose asChild>
<Button <Button variant="primary" onClick={() => updateEditorSettings(editorSettings)}>
variant="primary"
onClick={() => updateEditorSettings(editorSettings)}
>
Save changes Save changes
</Button> </Button>
</DialogClose> </DialogClose>
</Flex> </Flex>
<DialogClose asChild> <DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}> <Box css={{ position: 'absolute', top: '$3', right: '$3' }}>
<X size="20px" /> <X size="20px" />
</Box> </Box>
</DialogClose> </DialogClose>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</Flex> </Flex>
); )
}; }
export default EditorNavigation; 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,53 +1,53 @@
import { styled } from "../stitches.config"; import { styled } from '../stitches.config'
import Box from "./Box"; import Box from './Box'
const Flex = styled(Box, { const Flex = styled(Box, {
display: "flex", display: 'flex',
variants: { variants: {
row: { row: {
true: { true: {
flexDirection: "row", flexDirection: 'row'
}, }
}, },
column: { column: {
true: { true: {
flexDirection: "column", flexDirection: 'column'
}, }
}, },
fluid: { fluid: {
true: { true: {
width: "100%", width: '100%'
}, }
}, },
align: { align: {
start: { start: {
alignItems: "start", alignItems: 'start'
}, },
center: { center: {
alignItems: "center", alignItems: 'center'
}, },
end: { end: {
alignItems: "end", alignItems: 'end'
}, }
}, },
justify: { justify: {
start: { start: {
justifyContent: "start", justifyContent: 'start'
}, },
center: { center: {
justifyContent: "center", justifyContent: 'center'
}, },
end: { end: {
justifyContent: "end", justifyContent: 'end'
}, },
"space-between": { 'space-between': {
justifyContent: "space-between", justifyContent: 'space-between'
}, },
"space-around": { 'space-around': {
justifyContent: "space-around", justifyContent: 'space-around'
}, }
}, }
}, }
}); })
export default Flex; export default Flex

View File

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

View File

@@ -1,44 +1,45 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef, useState } from 'react'
import { useSnapshot, ref } from "valtio"; import { useSnapshot, ref } from 'valtio'
import Editor, { loader } from "@monaco-editor/react"; import type monaco from 'monaco-editor'
import type monaco from "monaco-editor"; import { ArrowBendLeftUp } from 'phosphor-react'
import { ArrowBendLeftUp } from "phosphor-react"; import { useTheme } from 'next-themes'
import { useTheme } from "next-themes"; import { useRouter } from 'next/router'
import { useRouter } from "next/router";
import Box from "./Box"; import Box from './Box'
import Container from "./Container"; import Container from './Container'
import dark from "../theme/editor/amy.json"; import { createNewFile, saveFile } from '../state/actions'
import light from "../theme/editor/xcode_default.json"; import { apiHeaderFiles } from '../state/constants'
import { saveFile } from "../state/actions"; import state from '../state'
import { apiHeaderFiles } from "../state/constants";
import state from "../state";
import EditorNavigation from "./EditorNavigation"; import EditorNavigation from './EditorNavigation'
import Text from "./Text"; import Text from './Text'
import { MonacoServices } from "@codingame/monaco-languageclient"; import { MonacoServices } from '@codingame/monaco-languageclient'
import { createLanguageClient, createWebSocket } from "../utils/languageClient"; import { createLanguageClient, createWebSocket } from '../utils/languageClient'
import { listen } from "@codingame/monaco-jsonrpc"; import { listen } from '@codingame/monaco-jsonrpc'
import ReconnectingWebSocket from "reconnecting-websocket"; import ReconnectingWebSocket from 'reconnecting-websocket'
import docs from "../xrpl-hooks-docs/docs"; 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'
loader.config({ const checkWritable = (filename?: string): boolean => {
paths: { if (apiHeaderFiles.find(file => file === filename)) {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs", return false
}, }
}); return true
}
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => { const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
const currPath = editor.getModel()?.uri.path; const filename = editor.getModel()?.uri.path.split('/').pop()
if (apiHeaderFiles.find((h) => currPath?.endsWith(h))) { const isWritable = checkWritable(filename)
editor.updateOptions({ readOnly: true }); editor.updateOptions({ readOnly: !isWritable })
} else { }
editor.updateOptions({ readOnly: false });
}
};
let decorations: { [key: string]: string[] } = {}; let decorations: { [key: string]: string[] } = {}
const setMarkers = (monacoE: typeof monaco) => { const setMarkers = (monacoE: typeof monaco) => {
// Get all the markers that are active at the moment, // Get all the markers that are active at the moment,
@@ -48,11 +49,11 @@ const setMarkers = (monacoE: typeof monaco) => {
.getModelMarkers({}) .getModelMarkers({})
// Filter out the markers that are hooks specific // Filter out the markers that are hooks specific
.filter( .filter(
(marker) => marker =>
typeof marker?.code === "string" && typeof marker?.code === 'string' &&
// Take only markers that starts with "hooks-" // Take only markers that starts with "hooks-"
marker?.code?.includes("hooks-") marker?.code?.includes('hooks-')
); )
// Get the active model (aka active file you're editing) // Get the active model (aka active file you're editing)
// const model = monacoE.editor?.getModel( // const model = monacoE.editor?.getModel(
@@ -61,17 +62,15 @@ const setMarkers = (monacoE: typeof monaco) => {
// console.log(state.active); // console.log(state.active);
// Add decoration (aka extra hoverMessages) to markers in the // Add decoration (aka extra hoverMessages) to markers in the
// exact same range (location) where the markers are // exact same range (location) where the markers are
const models = monacoE.editor.getModels(); const models = monacoE.editor.getModels()
models.forEach((model) => { models.forEach(model => {
decorations[model.id] = model?.deltaDecorations( decorations[model.id] = model?.deltaDecorations(
decorations?.[model.id] || [], decorations?.[model.id] || [],
markers markers
.filter((marker) => .filter(marker =>
marker?.resource.path marker?.resource.path.split('/').includes(`${state.files?.[state.active]?.name}`)
.split("/")
.includes(`${state.files?.[state.active]?.name}`)
) )
.map((marker) => ({ .map(marker => ({
range: new monacoE.Range( range: new monacoE.Range(
marker.startLineNumber, marker.startLineNumber,
marker.startColumn, marker.startColumn,
@@ -85,185 +84,239 @@ const setMarkers = (monacoE: typeof monaco) => {
// /xrpl-hooks-docs/xrpl-hooks-docs-files.json file // /xrpl-hooks-docs/xrpl-hooks-docs-files.json file
// which was generated from rst files // which was generated from rst files
(typeof marker.code === "string" && (typeof marker.code === 'string' && docs[marker?.code]?.toString()) || '',
docs[marker?.code]?.toString()) ||
"",
supportHtml: true, supportHtml: true,
isTrusted: true, isTrusted: true
}, }
}, }
})) }))
); )
}); })
}; }
const HooksEditor = () => { const HooksEditor = () => {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(); const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>()
const monacoRef = useRef<typeof monaco>(); const monacoRef = useRef<typeof monaco>()
const subscriptionRef = useRef<ReconnectingWebSocket | null>(null); const subscriptionRef = useRef<ReconnectingWebSocket | null>(null)
const snap = useSnapshot(state); const snap = useSnapshot(state)
const router = useRouter(); const router = useRouter()
const { theme } = useTheme(); const { theme } = useTheme()
const [isMdPreview, setIsMdPreview] = useState(true)
useEffect(() => { useEffect(() => {
if (editorRef.current) validateWritability(editorRef.current); if (editorRef.current) validateWritability(editorRef.current)
}, [snap.active]); }, [snap.active])
useEffect(() => { useEffect(() => {
return () => { return () => {
subscriptionRef?.current?.close(); subscriptionRef?.current?.close()
}; }
}, []); }, [])
useEffect(() => { useEffect(() => {
if (monacoRef.current) { if (monacoRef.current) {
setMarkers(monacoRef.current); setMarkers(monacoRef.current)
} }
}, [snap.active]); }, [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 ( return (
<Box <Box
css={{ css={{
flex: 1, flex: 1,
flexShrink: 1, flexShrink: 1,
display: "flex", display: 'flex',
position: "relative", position: 'relative',
flexDirection: "column", flexDirection: 'column',
backgroundColor: "$mauve2", backgroundColor: '$mauve2',
width: "100%", width: '100%'
}} }}
> >
<EditorNavigation /> <EditorNavigation renderNav={renderNav} />
{file?.language === 'markdown' && previewToggle}
{snap.files.length > 0 && router.isReady ? ( {snap.files.length > 0 && router.isReady ? (
<Editor isMdPreview && file?.language === 'markdown' ? (
className="hooks-editor" <Markdown
keepCurrentModel components={{
defaultLanguage={snap.files?.[snap.active]?.language} a: ({ href, children }) => (
language={snap.files?.[snap.active]?.language} <Link target="_blank" rel="noopener noreferrer" href={href}>
path={`file:///work/c/${snap.files?.[snap.active]?.name}`} {children}
defaultValue={snap.files?.[snap.active]?.content} </Link>
beforeMount={(monaco) => { )
if (!snap.editorCtx) { }}
snap.files.forEach((file) => >
monaco.editor.createModel( {file.content}
file.content, </Markdown>
file.language, ) : (
monaco.Uri.parse(`file:///work/c/${file.name}`) <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}`)
// )
// )
// }
// 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()
// create the web socket connection.onClose(() => {
if (!subscriptionRef.current) { try {
monaco.languages.register({ disposable.dispose()
id: "c", } catch (err) {
extensions: [".c", ".h"], console.log('err', err)
aliases: ["C", "c", "H", "h"], }
mimetypes: ["text/plain"], })
}); }
MonacoServices.install(monaco); })
const webSocket = createWebSocket(
process.env.NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT || ""
);
subscriptionRef.current = webSocket;
// listen when the web socket is opened
listen({
webSocket: webSocket as WebSocket,
onConnection: (connection) => {
// create and start the language client
const languageClient = createLanguageClient(connection);
const disposable = languageClient.start();
connection.onClose(() => {
try {
// disposable.stop();
disposable.dispose();
} catch (err) {
console.log("err", err);
}
});
},
});
}
// // hook editor to global state
// editor.updateOptions({
// minimap: {
// enabled: false,
// },
// ...snap.editorSettings,
// });
if (!state.editorCtx) {
state.editorCtx = ref(monaco.editor);
// @ts-expect-error
monaco.editor.defineTheme("dark", dark);
// @ts-expect-error
monaco.editor.defineTheme("light", light);
}
}}
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 // editor.updateOptions({
// Lets improve the markers by adding extra content to them from related // minimap: {
// md files // enabled: false,
monaco.editor.onDidChangeMarkers(() => { // },
if (monacoRef.current) { // ...snap.editorSettings,
setMarkers(monacoRef.current); // });
if (!state.editorCtx) {
state.editorCtx = ref(monaco.editor)
} }
}); }}
onMount={(editor, monaco) => {
// Hacky way to hide Peek menu editorRef.current = editor
editor.onContextMenu((e) => { monacoRef.current = monaco
const host = editor.updateOptions({
document.querySelector<HTMLElement>(".shadow-root-host"); glyphMargin: true,
lightbulb: {
const contextMenuItems = enabled: true
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";
} }
}); })
}); 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)
}
})
validateWritability(editor); // Hacky way to hide Peek menu
}} editor.onContextMenu(e => {
theme={theme === "dark" ? "dark" : "light"} 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> <Container>
{!snap.loading && router.isReady && ( {!snap.loading && router.isReady && (
<Box <Box
css={{ css={{
flexDirection: "row", flexDirection: 'row',
width: "$spaces$wide", width: '$spaces$wide',
gap: "$3", gap: '$3',
display: "inline-flex", display: 'inline-flex'
}} }}
> >
<Box css={{ display: "inline-flex", pl: "35px" }}> <Box css={{ display: 'inline-flex', pl: '35px' }}>
<ArrowBendLeftUp size={30} /> <ArrowBendLeftUp size={30} />
</Box> </Box>
<Box <Box css={{ pl: '0px', pt: '15px', flex: 1, display: 'inline-flex' }}>
css={{ pl: "0px", pt: "15px", flex: 1, display: "inline-flex" }}
>
<Text <Text
css={{ css={{
fontSize: "14px", fontSize: '14px',
maxWidth: "220px", maxWidth: '220px',
fontFamily: "$monospace", fontFamily: '$monospace'
}} }}
> >
Click the link above to create your file Click the link above to create your file
@@ -274,7 +327,7 @@ const HooksEditor = () => {
</Container> </Container>
)} )}
</Box> </Box>
); )
}; }
export default HooksEditor; export default HooksEditor

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,21 +1,15 @@
import { styled } from "../stitches.config"; import { styled } from '../stitches.config'
const SVG = styled("svg", { const SVG = styled('svg', {
"& #path": { '& #path': {
fill: "$accent", fill: '$accent'
}, }
}); })
function Logo({ function Logo({ width, height }: { width?: string | number; height?: string | number }) {
width,
height,
}: {
width?: string | number;
height?: string | number;
}) {
return ( return (
<SVG <SVG
width={width || "1.1em"} width={width || '1.1em'}
height={height || "1.1em"} height={height || '1.1em'}
viewBox="0 0 294 283" viewBox="0 0 294 283"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -28,7 +22,7 @@ function Logo({
fill="#9D2DFF" fill="#9D2DFF"
/> />
</SVG> </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,96 +1,90 @@
import React from "react"; import React from 'react'
import Link from "next/link"; import Link from 'next/link'
import { useSnapshot } from "valtio"; import { useSnapshot } from 'valtio'
import { useRouter } from "next/router"; import { useRouter } from 'next/router'
import { FolderOpen, X, ArrowUpRight, BookOpen } from "phosphor-react"; import { FolderOpen, X, ArrowUpRight, BookOpen } from 'phosphor-react'
import Stack from "./Stack"; import Stack from './Stack'
import Logo from "./Logo"; import Logo from './Logo'
import Button from "./Button"; import Button from './Button'
import Flex from "./Flex"; import Flex from './Flex'
import Container from "./Container"; import Container from './Container'
import Box from "./Box"; import Box from './Box'
import ThemeChanger from "./ThemeChanger"; import ThemeChanger from './ThemeChanger'
import state from "../state"; import state from '../state'
import Heading from "./Heading"; import Heading from './Heading'
import Text from "./Text"; import Text from './Text'
import Spinner from "./Spinner"; import Spinner from './Spinner'
import truncate from "../utils/truncate"; import truncate from '../utils/truncate'
import ButtonGroup from "./ButtonGroup"; import ButtonGroup from './ButtonGroup'
import { import {
Dialog, Dialog,
DialogClose, DialogClose,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger
} from "./Dialog"; } from './Dialog'
import PanelBox from "./PanelBox"; import PanelBox from './PanelBox'
import { templateFileIds } from "../state/constants"; import { templateFileIds } from '../state/constants'
import { styled } from "../stitches.config"; import { styled } from '../stitches.config'
import Starter from "../components/icons/Starter";
import Firewall from "../components/icons/Firewall";
import Notary from "../components/icons/Notary";
import Carbon from "../components/icons/Carbon";
import Peggy from "../components/icons/Peggy";
const ImageWrapper = styled(Flex, { const ImageWrapper = styled(Flex, {
position: "relative", position: 'relative',
mt: "$2", mt: '$2',
mb: "$10", mb: '$10',
svg: { svg: {
// fill: "red", // fill: "red",
".angle": { '.angle': {
fill: "$text", fill: '$text'
}, },
":not(.angle)": { ':not(.angle)': {
stroke: "$text", stroke: '$text'
}, }
}, }
}); })
const Navigation = () => { const Navigation = () => {
const router = useRouter(); const router = useRouter()
const snap = useSnapshot(state); const snap = useSnapshot(state)
const slug = router.query?.slug; const slug = router.query?.slug
const gistId = Array.isArray(slug) ? slug[0] : null; const gistId = Array.isArray(slug) ? slug[0] : null
return ( return (
<Box <Box
as="nav" as="nav"
css={{ css={{
display: "flex", display: 'flex',
backgroundColor: "$mauve1", backgroundColor: '$mauve1',
borderBottom: "1px solid $mauve6", borderBottom: '1px solid $mauve6',
position: "relative", position: 'relative',
zIndex: 2003, zIndex: 2003,
height: "60px", height: '60px'
}} }}
> >
<Container <Container
css={{ css={{
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center'
}} }}
> >
<Flex <Flex
css={{ css={{
flex: 1, flex: 1,
alignItems: "center", alignItems: 'center',
borderRight: "1px solid $colors$mauve6", borderRight: '1px solid $colors$mauve6',
py: "$3", py: '$3',
pr: "$4", pr: '$4'
}} }}
> >
<Link href={gistId ? `/develop/${gistId}` : "/develop"} passHref> <Link href={gistId ? `/develop/${gistId}` : '/develop'} passHref>
<Box <Box
as="a" as="a"
css={{ css={{
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
color: "$textColor", color: '$textColor'
}} }}
> >
<Logo width="32px" height="32px" /> <Logo width="32px" height="32px" />
@@ -98,38 +92,30 @@ const Navigation = () => {
</Link> </Link>
<Flex <Flex
css={{ css={{
ml: "$5", ml: '$5',
flexDirection: "column", flexDirection: 'column',
gap: "1px", gap: '1px'
}} }}
> >
{snap.loading ? ( {snap.loading ? (
<Spinner /> <Spinner />
) : ( ) : (
<> <>
<Heading css={{ lineHeight: 1 }}> <Heading css={{ lineHeight: 1 }}>{snap.gistName || 'Xahau Hooks'}</Heading>
{snap.files?.[0]?.name || "XRPL Hooks"} <Text css={{ fontSize: '$xs', color: '$mauve10', lineHeight: 1 }}>
</Heading> {snap.files.length > 0 ? 'Gist: ' : 'Builder'}
<Text
css={{ fontSize: "$xs", color: "$mauve10", lineHeight: 1 }}
>
{snap.files.length > 0 ? "Gist: " : "Builder"}
{snap.files.length > 0 && ( {snap.files.length > 0 && (
<Link <Link
href={`https://gist.github.com/${snap.gistOwner || ""}/${ href={`https://gist.github.com/${snap.gistOwner || ''}/${snap.gistId || ''}`}
snap.gistId || ""
}`}
passHref passHref
> >
<Text <Text
as="a" as="a"
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
css={{ color: "$mauve12" }} css={{ color: '$mauve12' }}
> >
{`${snap.gistOwner || "-"}/${truncate( {`${snap.gistOwner || '-'}/${truncate(snap.gistId || '')}`}
snap.gistId || ""
)}`}
</Text> </Text>
</Link> </Link>
)} )}
@@ -138,11 +124,8 @@ const Navigation = () => {
)} )}
</Flex> </Flex>
{router.isReady && ( {router.isReady && (
<ButtonGroup css={{ marginLeft: "auto" }}> <ButtonGroup css={{ marginLeft: 'auto' }}>
<Dialog <Dialog open={snap.mainModalOpen} onOpenChange={open => (state.mainModalOpen = open)}>
open={snap.mainModalOpen}
onOpenChange={(open) => (state.mainModalOpen = open)}
>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button outline> <Button outline>
<FolderOpen size="15px" /> <FolderOpen size="15px" />
@@ -150,123 +133,120 @@ const Navigation = () => {
</DialogTrigger> </DialogTrigger>
<DialogContent <DialogContent
css={{ css={{
display: "flex", display: 'flex',
maxWidth: "1080px", maxWidth: '1080px',
width: "80vw", width: '80vw',
maxHeight: "80%", maxHeight: '80%',
backgroundColor: "$mauve1 !important", backgroundColor: '$mauve1 !important',
overflowY: "auto", overflowY: 'auto',
background: "black", background: 'black',
p: 0, p: 0
}} }}
> >
<Flex <Flex
css={{ css={{
flexDirection: "column", flexDirection: 'column',
height: "100%", height: '100%',
"@md": { '@md': {
flexDirection: "row", flexDirection: 'row',
height: "100%", height: '100%'
}, }
}} }}
> >
<Flex <Flex
css={{ css={{
borderBottom: "1px solid $colors$mauve5", borderBottom: '1px solid $colors$mauve5',
width: "100%", width: '100%',
minWidth: "240px", minWidth: '240px',
flexDirection: "column", flexDirection: 'column',
p: "$7", p: '$7',
backgroundColor: "$mauve2", backgroundColor: '$mauve2',
"@md": { '@md': {
width: "30%", width: '30%',
maxWidth: "300px", maxWidth: '300px',
borderBottom: "0px", borderBottom: '0px',
borderRight: "1px solid $colors$mauve5", borderRight: '1px solid $colors$mauve5'
}, }
}} }}
> >
<DialogTitle <DialogTitle
css={{ css={{
textTransform: "uppercase", textTransform: 'uppercase',
display: "inline-flex", display: 'inline-flex',
alignItems: "center", alignItems: 'center',
gap: "$3", gap: '$3',
fontSize: "$xl", fontSize: '$xl',
lineHeight: "$one", lineHeight: '$one',
fontWeight: "$bold", fontWeight: '$bold'
}} }}
> >
<Logo width="48px" height="48px" /> XRPL Hooks Builder <Logo width="48px" height="48px" /> Xahau Hooks Builder
</DialogTitle> </DialogTitle>
<DialogDescription as="div"> <DialogDescription as="div">
<Text <Text
css={{ css={{
display: "inline-flex", display: 'inline-flex',
color: "inherit", color: 'inherit',
my: "$5", my: '$5',
mb: "$7", mb: '$7'
}} }}
> >
Hooks add smart contract functionality to the XRP Hooks add smart contract functionality to the Xahau Network.
Ledger.
</Text> </Text>
<Flex <Flex css={{ flexDirection: 'column', gap: '$2', mt: '$2' }}>
css={{ flexDirection: "column", gap: "$2", mt: "$2" }}
>
<Text <Text
css={{ css={{
display: "inline-flex", display: 'inline-flex',
alignItems: "center", alignItems: 'center',
gap: "$3", gap: '$3',
color: "$purple11", color: '$purple11',
"&:hover": { '&:hover': {
color: "$purple12", color: '$purple12'
},
"&:focus": {
outline: 0,
}, },
'&:focus': {
outline: 0
}
}} }}
as="a" as="a"
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
href="https://github.com/XRPL-Labs/xrpld-hooks" href="https://github.com/Xahau"
> >
<ArrowUpRight size="15px" /> Hooks Github <ArrowUpRight size="15px" /> Xahau Github
</Text> </Text>
<Text <Text
css={{ css={{
display: "inline-flex", display: 'inline-flex',
alignItems: "center", alignItems: 'center',
gap: "$3", gap: '$3',
color: "$purple11", color: '$purple11',
"&:hover": { '&:hover': {
color: "$purple12", color: '$purple12'
},
"&:focus": {
outline: 0,
}, },
'&:focus': {
outline: 0
}
}} }}
as="a" as="a"
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
href="https://xrpl-hooks.readme.io/v2.0/docs" href="https://docs.xahau.network/readme-1"
> >
<ArrowUpRight size="15px" /> Hooks documentation <ArrowUpRight size="15px" /> Hooks documentation
</Text> </Text>
<Text <Text
css={{ css={{
display: "inline-flex", display: 'inline-flex',
alignItems: "center", alignItems: 'center',
gap: "$3", gap: '$3',
color: "$purple11", color: '$purple11',
"&:hover": { '&:hover': {
color: "$purple12", color: '$purple12'
},
"&:focus": {
outline: 0,
}, },
'&:focus': {
outline: 0
}
}} }}
as="a" as="a"
rel="noreferrer noopener" rel="noreferrer noopener"
@@ -281,99 +261,47 @@ const Navigation = () => {
<Flex <Flex
css={{ css={{
display: "grid", display: 'grid',
gridTemplateColumns: "1fr", gridTemplateColumns: '1fr',
gridTemplateRows: "max-content", gridTemplateRows: 'max-content',
flex: 1, flex: 1,
p: "$7", p: '$7',
pb: "$16", pb: '$16',
gap: "$3", gap: '$3',
alignItems: "normal", alignItems: 'normal',
flexWrap: "wrap", flexWrap: 'wrap',
backgroundColor: "$mauve1", backgroundColor: '$mauve1',
"@md": { '@md': {
gridTemplateColumns: "1fr 1fr", gridTemplateColumns: '1fr 1fr',
gridTemplateRows: "max-content", gridTemplateRows: 'max-content'
},
"@lg": {
gridTemplateColumns: "1fr 1fr 1fr",
gridTemplateRows: "max-content",
}, },
'@lg': {
gridTemplateColumns: '1fr 1fr 1fr',
gridTemplateRows: 'max-content'
}
}} }}
> >
<PanelBox {Object.values(templateFileIds).map(template => (
as="a" <PanelBox key={template.id} as="a" href={`/develop/${template.id}`}>
href={`/develop/${templateFileIds.starter}`} <ImageWrapper>{template.icon()}</ImageWrapper>
> <Heading>{template.name}</Heading>
<ImageWrapper>
<Starter />
</ImageWrapper>
<Heading>Starter</Heading>
<Text> <Text>{template.description}</Text>
Just a basic starter with essential imports, just </PanelBox>
accepts any transaction coming through ))}
</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.firewall}`}
css={{ alignItems: "flex-start" }}
>
<ImageWrapper>
<Firewall />
</ImageWrapper>
<Heading>Firewall</Heading>
<Text>
This Hook essentially checks a blacklist of accounts
</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.notary}`}
>
<ImageWrapper>
<Notary />
</ImageWrapper>
<Heading>Notary</Heading>
<Text>
Collecting signatures for multi-sign transactions
</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.carbon}`}
>
<ImageWrapper>
<Carbon />
</ImageWrapper>
<Heading>Carbon</Heading>
<Text>Send a percentage of sum to an address</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.peggy}`}
>
<ImageWrapper>
<Peggy />
</ImageWrapper>
<Heading>Peggy</Heading>
<Text>An oracle based stable coin hook</Text>
</PanelBox>
</Flex> </Flex>
</Flex> </Flex>
<DialogClose asChild> <DialogClose asChild>
<Box <Box
css={{ css={{
position: "absolute", position: 'absolute',
top: "$1", top: '$1',
right: "$1", right: '$1',
cursor: "pointer", cursor: 'pointer',
background: "$mauve1", background: '$mauve1',
display: "flex", display: 'flex',
borderRadius: "$full", borderRadius: '$full',
p: "$1", p: '$1'
}} }}
> >
<X size="20px" /> <X size="20px" />
@@ -387,66 +315,44 @@ const Navigation = () => {
</Flex> </Flex>
<Flex <Flex
css={{ css={{
flexWrap: "nowrap", flexWrap: 'nowrap',
marginLeft: "$4", marginLeft: '$4',
overflowX: "scroll", overflowX: 'scroll',
"&::-webkit-scrollbar": { '&::-webkit-scrollbar': {
height: 0, height: 0,
background: "transparent", background: 'transparent'
}, },
scrollbarColor: 'transparent',
scrollbarWidth: 'none'
}} }}
> >
<Stack <Stack
css={{ css={{
ml: "$4", ml: '$4',
gap: "$3", gap: '$3',
flexWrap: "nowrap", flexWrap: 'nowrap',
alignItems: "center", alignItems: 'center',
marginLeft: "auto", marginLeft: 'auto'
}} }}
> >
<ButtonGroup> <ButtonGroup>
<Link <Link href={gistId ? `/develop/${gistId}` : '/develop'} passHref shallow>
href={gistId ? `/develop/${gistId}` : "/develop"} <Button as="a" outline={!router.pathname.includes('/develop')} uppercase>
passHref
shallow
>
<Button
as="a"
outline={!router.pathname.includes("/develop")}
uppercase
>
Develop Develop
</Button> </Button>
</Link> </Link>
<Link <Link href={gistId ? `/deploy/${gistId}` : '/deploy'} passHref shallow>
href={gistId ? `/deploy/${gistId}` : "/deploy"} <Button as="a" outline={!router.pathname.includes('/deploy')} uppercase>
passHref
shallow
>
<Button
as="a"
outline={!router.pathname.includes("/deploy")}
uppercase
>
Deploy Deploy
</Button> </Button>
</Link> </Link>
<Link <Link href={gistId ? `/test/${gistId}` : '/test'} passHref shallow>
href={gistId ? `/test/${gistId}` : "/test"} <Button as="a" outline={!router.pathname.includes('/test')} uppercase>
passHref
shallow
>
<Button
as="a"
outline={!router.pathname.includes("/test")}
uppercase
>
Test Test
</Button> </Button>
</Link> </Link>
</ButtonGroup> </ButtonGroup>
<Link href="https://xrpl-hooks.readme.io/v2.0" passHref> <Link href="https://xrpl-hooks.readme.io/" passHref>
<a target="_blank" rel="noreferrer noopener"> <a target="_blank" rel="noreferrer noopener">
<Button outline> <Button outline>
<BookOpen size="15px" /> <BookOpen size="15px" />
@@ -457,7 +363,7 @@ const Navigation = () => {
</Flex> </Flex>
</Container> </Container>
</Box> </Box>
); )
}; }
export default Navigation; export default Navigation

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,13 @@
import { forwardRef } from "react"; import { forwardRef } from 'react'
import { mauve, mauveDark, purple, purpleDark } from "@radix-ui/colors"; import { mauve, mauveDark, purple, purpleDark } from '@radix-ui/colors'
import { useTheme } from "next-themes"; import { useTheme } from 'next-themes'
import { styled } from "../stitches.config"; import { styled } from '../stitches.config'
import dynamic from "next/dynamic"; import dynamic from 'next/dynamic'
import type { Props } from "react-select"; import type { Props, StylesConfig } from 'react-select'
const SelectInput = dynamic(() => import("react-select"), { ssr: false }); const SelectInput = dynamic(() => import('react-select'), { ssr: false })
const CreatableSelectInput = dynamic(() => import('react-select/creatable'), { ssr: false })
// eslint-disable-next-line react/display-name const getColors = (isDark: boolean) => {
const Select = forwardRef<any, Props>((props, ref) => {
const { theme } = useTheme();
const isDark = theme === "dark";
const colors: any = { const colors: any = {
// primary: pink.pink9, // primary: pink.pink9,
active: isDark ? purpleDark.purple9 : purple.purple9, active: isDark ? purpleDark.purple9 : purple.purple9,
@@ -26,102 +24,140 @@ const Select = forwardRef<any, Props>((props, ref) => {
mauve9: isDark ? mauveDark.mauve9 : mauve.mauve9, mauve9: isDark ? mauveDark.mauve9 : mauve.mauve9,
mauve12: isDark ? mauveDark.mauve12 : mauve.mauve12, mauve12: isDark ? mauveDark.mauve12 : mauve.mauve12,
border: isDark ? mauveDark.mauve10 : mauve.mauve10, border: isDark ? mauveDark.mauve10 : mauve.mauve10,
placeholder: isDark ? mauveDark.mauve11 : mauve.mauve11, placeholder: isDark ? mauveDark.mauve11 : mauve.mauve11
}; }
colors.outline = colors.background; colors.outline = colors.background
colors.selected = colors.secondary; colors.selected = colors.secondary
return colors
}
const getStyles = (isDark: boolean) => {
const colors = getColors(isDark)
const styles: StylesConfig = {
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,
padding: 6,
color: state.isFocused ? colors.border : colors.secondary,
':hover': {
color: colors.border
}
}
},
clearIndicator: provided => {
return {
...provided,
padding: 6,
color: colors.secondary,
':hover': {
color: colors.border
}
}
}
}
return styles
}
// eslint-disable-next-line react/display-name
const Select = forwardRef<any, Props>((props, ref) => {
const { theme } = useTheme()
const isDark = theme === 'dark'
const styles = getStyles(isDark)
return ( return (
<SelectInput <SelectInput
ref={ref} ref={ref}
menuPosition={props.menuPosition || "fixed"} menuPosition={props.menuPosition || 'fixed'}
styles={{ styles={styles}
container: (provided) => {
return {
...provided,
position: "relative",
};
},
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.isSelected || state.isFocused
? colors.activeLight
: colors.dropDownBg,
":hover": {
backgroundColor: colors.active,
color: "#ffffff",
},
":selected": {
backgroundColor: "red",
},
};
},
indicatorSeparator: (provided) => {
return {
...provided,
backgroundColor: colors.secondary,
};
},
dropdownIndicator: (provided, state) => {
return {
...provided,
color: state.isFocused ? colors.border : colors.secondary,
":hover": {
color: colors.border,
},
};
},
}}
{...props} {...props}
/> />
); )
}); })
export default styled(Select, {}); // eslint-disable-next-line react/display-name
const Creatable = forwardRef<any, Props>((props, ref) => {
const { theme } = useTheme()
const isDark = theme === 'dark'
const styles = getStyles(isDark)
return (
<CreatableSelectInput
ref={ref}
formatCreateLabel={label => `Enter "${label}"`}
menuPosition={props.menuPosition || 'fixed'}
styles={styles}
{...props}
/>
)
})
export default styled(Select, {})
export const CreatableSelect = styled(Creatable, {})

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

@@ -1,227 +1,347 @@
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from 'react'
import { Plus, Trash, X } from "phosphor-react"; import { Plus, Trash, X } from 'phosphor-react'
import Button from "./Button"; import { Button, Box, Text } from '.'
import Box from "./Box"; import { Stack, Flex, Select } from '.'
import { Stack, Flex, Select } from ".";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
DialogDescription, DialogDescription,
DialogClose, DialogClose,
DialogTrigger, DialogTrigger
} from "./Dialog"; } from './Dialog'
import { Input, Label } from "./Input"; import { Input, Label } from './Input'
import { import { Controller, SubmitHandler, useFieldArray, useForm } from 'react-hook-form'
Controller,
SubmitHandler,
useFieldArray,
useForm,
} from "react-hook-form";
import { TTS, tts } from "../utils/hookOnCalculator"; import { deployHook } from '../state/actions'
import { deployHook } from "../state/actions"; import { useSnapshot } from 'valtio'
import type { IAccount } from "../state"; import state, { IFile, SelectOption } from '../state'
import { useSnapshot } from "valtio"; import toast from 'react-hot-toast'
import state from "../state"; import { prepareDeployHookTx, sha256 } from '../state/actions/deployHook'
import toast from "react-hot-toast"; import estimateFee from '../utils/estimateFee'
import { sha256 } from "../state/actions/deployHook"; import { getParameters, getInvokeOptions, transactionOptions, SetHookData } from '../utils/setHook'
import { capitalize } from '../utils/helpers'
import AccountSequence from './Sequence'
const transactionOptions = Object.keys(tts).map((key) => ({ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
label: key, ({ accountAddress }) => {
value: key as keyof TTS, const snap = useSnapshot(state)
}));
export type SetHookData = { const [estimateLoading, setEstimateLoading] = useState(false)
Invoke: { const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false)
value: keyof TTS;
label: string;
}[];
HookNamespace: string;
HookParameters: {
HookParameter: {
HookParameterName: string;
HookParameterValue: string;
};
}[];
// HookGrants: {
// HookGrant: {
// Authorize: string;
// HookHash: string;
// };
// }[];
};
export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => { const compiledFiles = snap.files.filter(file => file.compiledContent)
const snap = useSnapshot(state); const activeFile = compiledFiles[snap.activeWat] as IFile | undefined
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
const {
register,
handleSubmit,
control,
watch,
setValue,
formState: { errors },
} = useForm<SetHookData>({
defaultValues: {
HookNamespace: snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "",
},
});
const { fields, append, remove } = useFieldArray({
control,
name: "HookParameters", // unique name for your Field Array
});
// Update value if activeWat changes const accountOptions: SelectOption[] = snap.accounts.map(acc => ({
useEffect(() => { label: acc.name,
setValue( value: acc.address
"HookNamespace", }))
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
);
}, [snap.activeWat, snap.files, 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",
snap.files?.[snap.active]?.name?.split(".")?.[0] || ""
);
const calculateHashedValue = useCallback(async () => {
const hashedVal = await sha256(namespace);
setHashedNamespace(hashedVal.toUpperCase());
}, [namespace]);
useEffect(() => {
calculateHashedValue();
}, [namespace, calculateHashedValue]);
if (!account) { const [selectedAccount, setSelectedAccount] = useState(
return null; accountOptions.find(acc => acc.value === accountAddress)
} )
const account = snap.accounts.find(acc => acc.address === selectedAccount?.value)
const onSubmit: SubmitHandler<SetHookData> = async (data) => { const getHookNamespace = useCallback(
const currAccount = state.accounts.find( () =>
(acc) => acc.address === account.address (activeFile && snap.deployValues[activeFile.name]?.HookNamespace) ||
); activeFile?.name.split('.')[0] ||
if (currAccount) currAccount.isLoading = true; '',
const res = await deployHook(account, data); [activeFile, snap.deployValues]
if (currAccount) currAccount.isLoading = false; )
if (res && res.engine_result === "tesSUCCESS") { const getDefaultValues = useCallback((): Partial<SetHookData> => {
toast.success("Transaction succeeded!"); const content = activeFile?.compiledValueSnapshot
return setIsSetHookDialogOpen(false); 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
)
} }
toast.error(`Transaction failed! (${res?.engine_result_message})`);
};
return ( const onSubmit: SubmitHandler<SetHookData> = async data => {
<Dialog open={isSetHookDialogOpen} onOpenChange={setIsSetHookDialogOpen}> const currAccount = state.accounts.find(acc => acc.address === account?.address)
<DialogTrigger asChild> if (!account) return
<Button if (currAccount) currAccount.isLoading = true
ghost
size="xs" data.HookParameters.forEach(param => {
uppercase delete param.$metaData
variant={"secondary"} return param
disabled={ })
account.isLoading ||
!snap.files.filter((file) => file.compiledWatContent).length const res = await deployHook(account, data)
} if (currAccount) currAccount.isLoading = false
>
Set Hook if (res && res.engine_result === 'tesSUCCESS') {
</Button> toast.success('Transaction succeeded!')
</DialogTrigger> return setIsSetHookDialogOpen(false)
<DialogContent> }
<form onSubmit={handleSubmit(onSubmit)}> toast.error(`Transaction failed! (${res?.engine_result_message})`)
<DialogTitle>Deploy configuration</DialogTitle> }
<DialogDescription as="div">
<Stack css={{ width: "100%", flex: 1 }}> const onOpenChange = useCallback(
<Box css={{ width: "100%" }}> (open: boolean) => {
<Label>Invoke on transactions</Label> setIsSetHookDialogOpen(open)
<Controller
name="Invoke" if (open) calculateFee()
control={control} },
defaultValue={transactionOptions.filter( [calculateFee]
(to) => to.label === "ttPAYMENT" )
)} return (
render={({ field }) => ( <Dialog open={isSetHookDialogOpen} onOpenChange={onOpenChange}>
<Select <DialogTrigger asChild>
{...field} <Button
closeMenuOnSelect={false} ghost
isMulti size="xs"
menuPosition="fixed" uppercase
options={transactionOptions} variant={'secondary'}
/> disabled={!account || account.isLoading || !activeFile || tooLargeFile()}
)} >
/> Set Hook
</Box> </Button>
<Box css={{ width: "100%" }}> </DialogTrigger>
<Label>Hook Namespace Seed</Label> <DialogContent>
<Input <form onSubmit={handleSubmit(onSubmit)}>
{...register("HookNamespace", { required: true })} <DialogTitle>Deploy configuration</DialogTitle>
autoComplete={"off"} <DialogDescription as="div">
defaultValue={ <Stack css={{ width: '100%', flex: 1 }}>
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "" <Box css={{ width: '100%' }}>
} <Label>Account</Label>
/> <Select
{errors.HookNamespace?.type === "required" && ( instanceId="deploy-account"
<Box css={{ display: "inline", color: "$red11" }}> placeholder="Select account"
Namespace is required options={accountOptions}
</Box> value={selectedAccount}
)} onChange={(acc: any) => setSelectedAccount(acc)}
<Box css={{ mt: "$3" }}> />
<Label>Hook Namespace (sha256)</Label>
<Input readOnly value={hashedNamespace} />
</Box> </Box>
</Box> <Box css={{ width: '100%', position: 'relative' }}>
<Box css={{ width: "100%" }}> <Label>Sequence</Label>
<Label style={{ marginBottom: "10px", display: "block" }}> <AccountSequence address={selectedAccount?.value} />
Hook parameters </Box>
</Label> <Box css={{ width: '100%' }}>
<Stack> <Label>Invoke on transactions</Label>
{fields.map((field, index) => ( <Controller
<Stack key={field.id}> name="Invoke"
<Input control={control}
// important to include key with field's id render={({ field }) => (
placeholder="Parameter name" <Select
{...register( {...field}
`HookParameters.${index}.HookParameter.HookParameterName` closeMenuOnSelect={false}
)} isMulti
menuPosition="fixed"
options={transactionOptions}
/> />
<Input )}
placeholder="Value (hex-quoted)" />
{...register( </Box>
`HookParameters.${index}.HookParameter.HookParameterValue` <Box css={{ width: '100%' }}>
)} <Label>Hook Namespace Seed</Label>
/> <Input {...register('HookNamespace', { required: true })} autoComplete={'off'} />
<Button onClick={() => remove(index)} variant="destroy"> {errors.HookNamespace?.type === 'required' && (
<Trash weight="regular" size="16px" /> <Box css={{ display: 'inline', color: '$red11' }}>Namespace is required</Box>
</Button> )}
</Stack> <Box css={{ mt: '$3' }}>
))} <Label>Hook Namespace (sha256)</Label>
<Button <Input readOnly value={hashedNamespace} />
outline </Box>
fullWidth </Box>
type="button"
onClick={() => <Box css={{ width: '100%' }}>
append({ <Label style={{ marginBottom: '10px', display: 'block' }}>Hook parameters</Label>
HookParameter: { <Stack>
HookParameterName: "", {fields.map((field, index) => (
HookParameterValue: "", <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
<Plus size="16px" /> }
Add Hook Parameter }}
</Button> />
</Stack> <Button
</Box> size="xs"
{/* <Box css={{ width: "100%" }}> 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" }}> <label style={{ marginBottom: "10px", display: "block" }}>
Hook Grants Hook Grants
</label> </label>
@@ -269,38 +389,37 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
</Button> </Button>
</Stack> </Stack>
</Box> */} </Box> */}
</Stack> </Stack>
</DialogDescription> </DialogDescription>
<Flex <Flex
css={{ css={{
marginTop: 25, marginTop: 25,
justifyContent: "flex-end", justifyContent: 'flex-end',
gap: "$3", gap: '$3'
}} }}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
{/* <DialogClose asChild> */}
<Button
variant="primary"
type="submit"
isLoading={account.isLoading}
> >
Set Hook <DialogClose asChild>
</Button> <Button outline>Cancel</Button>
{/* </DialogClose> */} </DialogClose>
</Flex> {/* <DialogClose asChild> */}
<DialogClose asChild> <Button variant="primary" type="submit" isLoading={account?.isLoading}>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}> Set Hook
<X size="20px" /> </Button>
</Box> {/* </DialogClose> */}
</DialogClose> </Flex>
</form> <DialogClose asChild>
</DialogContent> <Box css={{ position: 'absolute', top: '$3', right: '$3' }}>
</Dialog> <X size="20px" />
); </Box>
}; </DialogClose>
</form>
</DialogContent>
</Dialog>
)
}
)
export default SetHookDialog; SetHookDialog.displayName = 'SetHookDialog'
export default SetHookDialog

View File

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

View File

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

View File

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

View File

@@ -1,51 +1,58 @@
import React, { import React, { useEffect, useState, Fragment, isValidElement, useCallback } from 'react'
useEffect, import type { ReactNode, ReactElement } from 'react'
useState, import { Box, Button, Flex, Input, Label, Pre, Stack, Text } from '.'
Fragment,
isValidElement,
useCallback,
} from "react";
import type { ReactNode, ReactElement } from "react";
import { Box, Button, Flex, Input, Label, Stack, Text } from ".";
import { import {
Dialog, Dialog,
DialogTrigger, DialogTrigger,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
DialogDescription, DialogDescription,
DialogClose, DialogClose
} from "./Dialog"; } from './Dialog'
import { Plus, X } from "phosphor-react"; import { Plus, X } from 'phosphor-react'
import { styled } from "../stitches.config"; import { styled } from '../stitches.config'
import { capitalize, getFileExtention } from '../utils/helpers'
import ContextMenu, { ContentMenuOption } from './ContextMenu'
const ErrorText = styled(Text, { const ErrorText = styled(Text, {
color: "$error", color: '$error',
mt: "$1", mt: '$1',
display: "block", display: 'block'
}); })
type Nullable<T> = T | null | undefined | false
interface TabProps { interface TabProps {
header?: string; header: string
children: ReactNode; children?: ReactNode
renameDisabled?: boolean
} }
// TODO customise messages shown // TODO customize messages shown
interface Props { interface Props {
activeIndex?: number; label?: string
activeHeader?: string; activeIndex?: number
headless?: boolean; activeHeader?: string
children: ReactElement<TabProps>[]; headless?: boolean
keepAllAlive?: boolean; children: ReactElement<TabProps>[]
defaultExtension?: string; keepAllAlive?: boolean
forceDefaultExtension?: boolean; defaultExtension?: string
onCreateNewTab?: (name: string) => any; extensionRequired?: boolean
onCloseTab?: (index: number, header?: string) => any; allowedExtensions?: string[]
onChangeActive?: (index: number, header?: string) => any; 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 Tab = (props: TabProps) => null
export const Tabs = ({ export const Tabs = ({
label = 'Tab',
children, children,
activeIndex, activeIndex,
activeHeader, activeHeader,
@@ -54,182 +61,229 @@ export const Tabs = ({
onCreateNewTab, onCreateNewTab,
onCloseTab, onCloseTab,
onChangeActive, onChangeActive,
defaultExtension = "", onRenameTab,
forceDefaultExtension, headerExtraValidation,
extensionRequired,
defaultExtension = '',
allowedExtensions
}: Props) => { }: Props) => {
const [active, setActive] = useState(activeIndex || 0); const [active, setActive] = useState(activeIndex || 0)
const tabs: TabProps[] = children.map(elem => elem.props); const tabs: TabProps[] = children.map(elem => elem.props)
const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false); const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false)
const [tabname, setTabname] = useState(""); const [renamingTab, setRenamingTab] = useState<number | null>(null)
const [newtabError, setNewtabError] = useState<string | null>(null); const [tabname, setTabname] = useState('')
const [tabnameError, setTabnameError] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
if (activeIndex) setActive(activeIndex); if (activeIndex) setActive(activeIndex)
}, [activeIndex]); }, [activeIndex])
useEffect(() => { useEffect(() => {
if (activeHeader) { if (activeHeader) {
const idx = tabs.findIndex(tab => tab.header === activeHeader); const idx = tabs.findIndex(tab => tab.header === activeHeader)
if (idx !== -1) setActive(idx); if (idx !== -1) setActive(idx)
else setActive(0); else setActive(0)
} }
}, [activeHeader, tabs]); }, [activeHeader, tabs])
// when filename changes, reset error // when filename changes, reset error
useEffect(() => { useEffect(() => {
setNewtabError(null); setTabnameError(null)
}, [tabname, setNewtabError]); }, [tabname, setTabnameError])
const validateTabname = useCallback( const validateTabname = useCallback(
(tabname: string): { error: string | null } => { (tabname: string): { error?: string; result?: string } => {
if (tabs.find(tab => tab.header === tabname)) { if (!tabname) {
return { error: "Name already exists." }; return { error: `Please enter ${label.toLocaleLowerCase()} name.` }
} }
return { error: null }; 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 }
}, },
[tabs] [allowedExtensions, defaultExtension, extensionRequired, headerExtraValidation, label, tabs]
); )
const handleActiveChange = useCallback( const handleActiveChange = useCallback(
(idx: number, header?: string) => { (idx: number, header?: string) => {
setActive(idx); setActive(idx)
onChangeActive?.(idx, header); onChangeActive?.(idx, header)
}, },
[onChangeActive] [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 handleCreateTab = useCallback(() => {
// add default extension in case omitted const res = validateTabname(tabname)
let _tabname = tabname.includes(".") ? tabname : tabname + defaultExtension; if (res.error) {
if (forceDefaultExtension && !_tabname.endsWith(defaultExtension)) { setTabnameError(`Error: ${res.error}`)
_tabname = _tabname + defaultExtension; return
} }
const { result: _tabname = tabname } = res
const chk = validateTabname(_tabname); setIsNewtabDialogOpen(false)
if (chk.error) { setTabname('')
setNewtabError(`Error: ${chk.error}`);
return;
}
setIsNewtabDialogOpen(false); onCreateNewTab?.(_tabname)
setTabname("");
onCreateNewTab?.(_tabname); handleActiveChange(tabs.length, _tabname)
}, [validateTabname, tabname, onCreateNewTab, handleActiveChange, tabs.length])
// switch to new tab?
handleActiveChange(tabs.length, _tabname);
}, [
tabname,
defaultExtension,
forceDefaultExtension,
validateTabname,
onCreateNewTab,
handleActiveChange,
tabs.length,
]);
const handleCloseTab = useCallback( const handleCloseTab = useCallback(
(idx: number) => { (idx: number) => {
if (idx <= active && active !== 0) { onCloseTab?.(idx, tabs[idx].header)
setActive(active - 1);
}
onCloseTab?.(idx, tabs[idx].header); if (idx <= active && active !== 0) {
const nwActive = active - 1
handleActiveChange(nwActive, tabs[nwActive].header)
}
}, },
[active, onCloseTab, tabs] [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 ( return (
<> <>
{!headless && ( {!headless && (
<Stack <Stack
css={{ css={{
gap: "$3", gap: '$3',
flex: 1, flex: 1,
flexWrap: "nowrap", flexWrap: 'nowrap',
marginBottom: "$2", marginBottom: '$2',
width: "100%", width: '100%',
overflow: "auto", overflow: 'auto'
}} }}
> >
{tabs.map((tab, idx) => ( {tabs.map((tab, idx) => (
<Button <ContextMenu
key={tab.header} key={tab.header}
role="tab" options={
tabIndex={idx} [closeOption(idx), renameOption(idx, tab)].filter(Boolean) as ContentMenuOption[]
onClick={() => handleActiveChange(idx, tab.header)} }
onKeyPress={() => handleActiveChange(idx, tab.header)}
outline={active !== idx}
size="sm"
css={{
"&:hover": {
span: {
visibility: "visible",
},
},
}}
> >
{tab.header || idx} <Button
{onCloseTab && ( role="tab"
<Box tabIndex={idx}
as="span" onClick={() => handleActiveChange(idx, tab.header)}
css={{ onKeyPress={() => handleActiveChange(idx, tab.header)}
display: "flex", outline={active !== idx}
p: "2px", size="sm"
borderRadius: "$full", css={{
mr: "-4px", '&:hover': {
"&:hover": { span: {
// boxSizing: "0px 0px 1px", visibility: 'visible'
backgroundColor: "$mauve2", }
color: "$mauve12", }
}, }}
}} >
onClick={(ev: React.MouseEvent<HTMLElement>) => { {tab.header || idx}
ev.stopPropagation(); {onCloseTab && (
handleCloseTab(idx); <Box
}} as="span"
> css={{
<X size="9px" weight="bold" /> display: 'flex',
</Box> p: '2px',
)} borderRadius: '$full',
</Button> 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 && ( {onCreateNewTab && (
<Dialog <Dialog open={isNewtabDialogOpen} onOpenChange={setIsNewtabDialogOpen}>
open={isNewtabDialogOpen}
onOpenChange={setIsNewtabDialogOpen}
>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button <Button ghost size="sm" css={{ alignItems: 'center', px: '$2', mr: '$3' }}>
ghost <Plus size="16px" /> {tabs.length === 0 && `Add new ${label.toLocaleLowerCase()}`}
size="sm"
css={{ alignItems: "center", px: "$2", mr: "$3" }}
>
<Plus size="16px" /> {tabs.length === 0 && "Add new tab"}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogTitle>Create new tab</DialogTitle> <DialogTitle>Create new {label.toLocaleLowerCase()}</DialogTitle>
<DialogDescription> <DialogDescription>
<Label>Tabname</Label> <Label>{label} name</Label>
<Input <Input
value={tabname} value={tabname}
onChange={e => setTabname(e.target.value)} onChange={e => setTabname(e.target.value)}
onKeyPress={e => { onKeyPress={e => {
if (e.key === "Enter") { if (e.key === 'Enter') {
handleCreateTab(); handleCreateTab()
} }
}} }}
/> />
<ErrorText>{newtabError}</ErrorText> <ErrorText>{tabnameError}</ErrorText>
</DialogDescription> </DialogDescription>
<Flex <Flex
css={{ css={{
marginTop: 25, marginTop: 25,
justifyContent: "flex-end", justifyContent: 'flex-end',
gap: "$3", gap: '$3'
}} }}
> >
<DialogClose asChild> <DialogClose asChild>
@@ -240,7 +294,49 @@ export const Tabs = ({
</Button> </Button>
</Flex> </Flex>
<DialogClose asChild> <DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}> <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" /> <X size="20px" />
</Box> </Box>
</DialogClose> </DialogClose>
@@ -249,29 +345,30 @@ export const Tabs = ({
)} )}
</Stack> </Stack>
)} )}
{keepAllAlive ? ( {keepAllAlive
tabs.map((tab, idx) => { ? tabs.map((tab, idx) => {
// TODO Maybe rule out fragments as children // TODO Maybe rule out fragments as children
if (!isValidElement(tab.children)) { if (!isValidElement(tab.children)) {
if (active !== idx) return null; if (active !== idx) return null
return tab.children; return tab.children
} }
let key = tab.children.key || tab.header || idx; let key = tab.children.key || tab.header || idx
let { children } = tab; let { children } = tab
let { style, ...props } = children.props; let { style, ...props } = children.props
return ( return (
<children.type <children.type
key={key} key={key}
{...props} {...props}
style={{ ...style, display: active !== idx ? "none" : undefined }} style={{
/> ...style,
); display: active !== idx ? 'none' : undefined
}) }}
) : ( />
<Fragment key={tabs[active].header || active}> )
{tabs[active].children} })
</Fragment> : tabs[active] && (
)} <Fragment key={tabs[active].header || active}>{tabs[active].children}</Fragment>
)}
</> </>
); )
}; }

View File

@@ -1,9 +1,9 @@
import { styled } from "../stitches.config"; import { styled } from '../stitches.config'
const Text = styled("span", { const Text = styled('span', {
fontFamily: "$body", fontFamily: '$body',
lineHeight: "$body", lineHeight: '$body',
color: "$text", color: '$text',
variants: { variants: {
small: { small: {
true: { true: {
@@ -15,12 +15,27 @@ const Text = styled("span", {
color: '$mauve9' color: '$mauve9'
} }
}, },
error: {
true: {
color: '$error'
}
},
warning: {
true: {
color: '$warning'
}
},
monospace: { monospace: {
true: { true: {
fontFamily: '$monospace' 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,34 +1,34 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from 'react'
import { useTheme } from "next-themes"; import { useTheme } from 'next-themes'
import { Sun, Moon } from "phosphor-react"; import { Sun, Moon } from 'phosphor-react'
import Button from "./Button"; import Button from './Button'
const ThemeChanger = () => { const ThemeChanger = () => {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), []); useEffect(() => setMounted(true), [])
if (!mounted) return null; if (!mounted) return null
return ( return (
<Button <Button
outline outline
onClick={() => { onClick={() => {
setTheme(theme && theme === "light" ? "dark" : "light"); setTheme(theme && theme === 'light' ? 'dark' : 'light')
}} }}
css={{ css={{
display: "flex", display: 'flex',
marginLeft: "auto", marginLeft: 'auto',
cursor: "pointer", cursor: 'pointer',
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
color: "$muted", color: '$muted'
}} }}
> >
{theme === "dark" ? <Sun size="15px" /> : <Moon size="15px" />} {theme === 'dark' ? <Sun size="15px" /> : <Moon size="15px" />}
</Button> </Button>
); )
}; }
export default ThemeChanger; export default ThemeChanger

View File

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

View File

@@ -1,169 +1,258 @@
import { Play } from "phosphor-react"; import { Play } from 'phosphor-react'
import { FC, useCallback, useEffect, useMemo } from "react"; import { FC, useCallback, useEffect } from 'react'
import { useSnapshot } from "valtio"; import { useSnapshot } from 'valtio'
import state from "../../state"; import state from '../../state'
import { import {
modifyTransaction, defaultTransactionType,
getTxFields,
modifyTxState,
prepareState, prepareState,
prepareTransaction, prepareTransaction,
TransactionState, SelectOption,
} from "../../state/transactions"; TransactionState
import { sendTransaction } from "../../state/actions"; } from '../../state/transactions'
import Box from "../Box"; import { sendTransaction } from '../../state/actions'
import Button from "../Button"; import Box from '../Box'
import Flex from "../Flex"; import Button from '../Button'
import { TxJson } from "./json"; import Flex from '../Flex'
import { TxUI } from "./ui"; 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 { export interface TransactionProps {
header: string; header: string
state: TransactionState; state: TransactionState
} }
const Transaction: FC<TransactionProps> = ({ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props }) => {
header, const { accounts, editorSettings } = useSnapshot(state)
state: txState, const { selectedAccount, selectedTransaction, txIsDisabled, txIsLoading, viewType, editorValue } =
...props txState
}) => {
const { accounts, editorSettings } = useSnapshot(state);
const {
selectedAccount,
selectedTransaction,
txIsDisabled,
txIsLoading,
viewType,
editorSavedValue,
editorValue,
} = txState;
const setState = useCallback( const setState = useCallback(
(pTx?: Partial<TransactionState>) => { (pTx?: Partial<TransactionState>) => {
return modifyTransaction(header, pTx); return modifyTxState(header, pTx)
}, },
[header] [header]
); )
const prepareOptions = useCallback( const prepareOptions = useCallback(
(state: TransactionState = txState) => { (state: Partial<TransactionState> = txState) => {
const { const {
selectedTransaction, selectedTransaction,
selectedDestAccount,
selectedAccount, selectedAccount,
txFields, txFields,
} = state; selectedFlags,
hookParameters,
memos
} = state
const TransactionType = selectedTransaction?.value || null; const TransactionType = selectedTransaction?.value || null
const Destination = const Account = selectedAccount?.value || null
selectedDestAccount?.value || const Flags = combineFlags(selectedFlags?.map(flag => flag.value)) || txFields?.Flags
("Destination" in txFields ? null : undefined); const HookParameters = Object.entries(hookParameters || {}).reduce<
const Account = selectedAccount?.value || null; SetHookData['HookParameters']
>((acc, [_, { label, value }]) => {
return acc.concat({
HookParameter: { HookParameterName: toHex(label), HookParameterValue: value }
})
}, [])
const Memos = memos
? Object.entries(memos).reduce<SetHookData['Memos']>((acc, [_, { format, data, type }]) => {
return acc?.concat({
Memo: { MemoData: data, MemoFormat: toHex(format), MemoType: toHex(type) }
})
}, [])
: undefined
return prepareTransaction({ return prepareTransaction({
...txFields, ...txFields,
HookParameters,
Flags,
TransactionType, TransactionType,
Destination,
Account, Account,
}); Memos
})
}, },
[txState] [txState]
); )
useEffect(() => { useEffect(() => {
const transactionType = selectedTransaction?.value; const transactionType = selectedTransaction?.value
const account = selectedAccount?.value; const account = selectedAccount?.value
if (!account || !transactionType || txIsLoading) { if (!account || !transactionType || txIsLoading) {
setState({ txIsDisabled: true }); setState({ txIsDisabled: true })
} else { } else {
setState({ txIsDisabled: false }); setState({ txIsDisabled: false })
} }
}, [selectedAccount?.value, selectedTransaction?.value, setState, txIsLoading]); }, [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 () => { const submitTest = useCallback(async () => {
let st: TransactionState | undefined; let st: TransactionState | undefined
if (viewType === "json") { const tt = txState.selectedTransaction?.value
// save the editor state first if (viewType === 'json') {
const pst = prepareState(editorValue || '', txState); st = saveEditorState(editorValue, tt)
if (!pst) return; if (!st) return
st = setState(pst);
} }
const account = accounts.find( const account = accounts.find(acc => acc.address === selectedAccount?.value)
acc => acc.address === selectedAccount?.value if (txIsDisabled) return
);
if (txIsDisabled) return;
setState({ txIsLoading: true }); setState({ txIsLoading: true })
const logPrefix = header ? `${header.split(".")[0]}: ` : undefined; const logPrefix = header ? `${header.split('.')[0]}: ` : undefined
try { try {
if (!account) { if (!account) {
throw Error("Account must be selected from imported accounts!"); throw Error('Account must be selected from imported accounts!')
} }
const options = prepareOptions(st); const options = prepareOptions(st)
// delete unnecessary fields
Object.keys(options).forEach(field => {
if (!options[field]) {
delete options[field]
}
})
if (options.Destination === null) { await sendTransaction(account, options, { logPrefix })
throw Error("Destination account cannot be null")
}
await sendTransaction(account, options, { logPrefix });
} catch (error) { } catch (error) {
console.error(error); console.error(error)
if (error instanceof Error) { if (error instanceof Error) {
state.transactionLogs.push({ state.transactionLogs.push({
type: "error", type: 'error',
message: `${logPrefix}${error.message}`, message: `${logPrefix}${error.message}`
}); })
} }
} }
setState({ txIsLoading: false }); setState({ txIsLoading: false })
}, [viewType, accounts, txIsDisabled, setState, header, editorValue, txState, selectedAccount?.value, prepareOptions]); }, [
txState.selectedTransaction?.value,
viewType,
accounts,
txIsDisabled,
setState,
header,
saveEditorState,
editorValue,
selectedAccount?.value,
prepareOptions
])
const resetState = useCallback(() => { const resetState = useCallback(
modifyTransaction(header, { viewType }, { replaceState: true }); (transactionType: SelectOption | undefined = defaultTransactionType) => {
}, [header, viewType]); const fields = getTxFields(transactionType?.value)
const jsonValue = useMemo( const nwState: Partial<TransactionState> = {
() => viewType,
editorSavedValue || selectedTransaction: transactionType
JSON.stringify(prepareOptions?.() || {}, null, editorSettings.tabSize), }
[editorSavedValue, editorSettings.tabSize, prepareOptions]
); 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 ( return (
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}> <Box css={{ position: 'relative', height: 'calc(100% - 28px)' }} {...props}>
{viewType === "json" ? ( {viewType === 'json' ? (
<TxJson <TxJson
value={jsonValue} getJsonString={getJsonString}
saveEditorState={saveEditorState}
header={header} header={header}
state={txState} state={txState}
setState={setState} setState={setState}
estimateFee={estimateFee}
/> />
) : ( ) : (
<TxUI state={txState} setState={setState} /> <TxUI
switchToJson={switchToJson}
state={txState}
resetState={resetState}
setState={setState}
estimateFee={estimateFee}
/>
)} )}
<Flex <Flex
row row
css={{ css={{
justifyContent: "space-between", justifyContent: 'space-between',
position: "absolute", position: 'absolute',
left: 0, left: 0,
bottom: 0, bottom: 0,
width: "100%", width: '100%',
mb: "$1", mb: '$1'
}} }}
> >
<Button <Button
onClick={() => { onClick={() => {
if (viewType === "ui") { if (viewType === 'ui') {
setState({ editorSavedValue: null, viewType: "json" }); switchToJson()
} else setState({ viewType: "ui" }); } else switchToUI()
}} }}
outline outline
> >
{viewType === "ui" ? "EDIT AS JSON" : "EXIT JSON MODE"} {viewType === 'ui' ? 'EDIT AS JSON' : 'EXIT JSON MODE'}
</Button> </Button>
<Flex row> <Flex row>
<Button onClick={resetState} outline css={{ mr: "$3" }}> <Button onClick={() => resetState()} outline css={{ mr: '$3' }}>
RESET RESET
</Button> </Button>
<Button <Button
@@ -178,7 +267,7 @@ const Transaction: FC<TransactionProps> = ({
</Flex> </Flex>
</Flex> </Flex>
</Box> </Box>
); )
}; }
export default Transaction; export default Transaction

View File

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

View File

@@ -1,213 +1,583 @@
import { FC } from "react"; import { FC, ReactNode, useCallback, useEffect, useState } from 'react'
import Container from "../Container"; import Container from '../Container'
import Flex from "../Flex"; import Flex from '../Flex'
import Input from "../Input"; import Input from '../Input'
import Select from "../Select"; import Select, { CreatableSelect } from '../Select'
import Text from "../Text"; import Text from '../Text'
import { import {
SelectOption, SelectOption,
TransactionState, TransactionState,
transactionsData, transactionsOptions,
TxFields, TxFields,
} from "../../state/transactions"; defaultTransactionType
import { useSnapshot } from "valtio"; } from '../../state/transactions'
import state from "../../state"; import { useSnapshot } from 'valtio'
import { streamState } from "../DebugStream"; import state from '../../state'
import { streamState } from '../DebugStream'
import { Box, Button } from '..'
import Textarea from '../Textarea'
import { getFlags } from '../../state/constants/flags'
import { Plus, Trash } from 'phosphor-react'
import AccountSequence from '../Sequence'
import { capitalize, typeIs } from '../../utils/helpers'
interface UIProps { interface UIProps {
setState: (pTx?: Partial<TransactionState> | undefined) => void; setState: (pTx?: Partial<TransactionState> | undefined) => TransactionState | undefined
state: TransactionState; resetState: (tt?: SelectOption) => TransactionState | undefined
state: TransactionState
estimateFee?: (...arg: any) => Promise<string | undefined>
switchToJson: () => void
} }
export const TxUI: FC<UIProps> = ({ state: txState, setState }) => { export const TxUI: FC<UIProps> = ({
const { accounts } = useSnapshot(state); state: txState,
const { setState,
selectedAccount, resetState,
selectedDestAccount, estimateFee,
selectedTransaction, switchToJson
txFields, }) => {
} = txState; const { accounts } = useSnapshot(state)
const { selectedAccount, selectedTransaction, txFields, selectedFlags, hookParameters, memos } =
const transactionsOptions = transactionsData.map(tx => ({ txState
value: tx.TransactionType,
label: tx.TransactionType,
}));
const accountOptions: SelectOption[] = accounts.map(acc => ({ const accountOptions: SelectOption[] = accounts.map(acc => ({
label: acc.name, label: acc.name,
value: acc.address, value: acc.address
})); }))
const destAccountOptions: SelectOption[] = accounts const flagsOptions: SelectOption[] = Object.entries(
.map(acc => ({ getFlags(selectedTransaction?.value) || {}
label: acc.name, ).map(([label, value]) => ({
value: acc.address, label,
})) value
.filter(acc => acc.value !== selectedAccount?.value); }))
const resetOptions = (tt: string) => { const [feeLoading, setFeeLoading] = useState(false)
const txFields: TxFields | undefined = transactionsData.find(
tx => tx.TransactionType === tt
);
if (!txFields) return setState({ txFields: {} });
const _txFields = Object.keys(txFields)
.filter(key => !["TransactionType", "Account", "Sequence"].includes(key))
.reduce<TxFields>(
(tf, key) => ((tf[key as keyof TxFields] = (txFields as any)[key]), tf),
{}
);
if (!_txFields.Destination) setState({ selectedDestAccount: null });
setState({ txFields: _txFields });
};
const handleSetAccount = (acc: SelectOption) => { const handleSetAccount = (acc: SelectOption) => {
setState({ selectedAccount: acc }); setState({ selectedAccount: acc })
streamState.selectedAccount = acc; streamState.selectedAccount = acc
}; }
const handleChangeTxType = (tt: SelectOption) => { const handleSetField = useCallback(
setState({ selectedTransaction: tt }); (field: keyof TxFields, value: string, opFields?: TxFields) => {
resetOptions(tt.value); const fields = opFields || txFields
}; const obj = fields[field]
setState({
txFields: {
...fields,
[field]: typeof obj === 'object' ? { ...obj, $value: value } : value
}
})
},
[setState, txFields]
)
const specialFields = ["TransactionType", "Account", "Destination"]; const setRawField = useCallback(
(field: keyof TxFields, type: string, value: any) => {
// TODO $type should be a narrowed type
setState({
txFields: {
...txFields,
[field]: {
$type: type,
$value: value
}
}
})
},
[setState, txFields]
)
const otherFields = Object.keys(txFields).filter( const handleEstimateFee = useCallback(
k => !specialFields.includes(k) async (state?: TransactionState, silent?: boolean) => {
) as [keyof TxFields]; 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 richFields = ['TransactionType', 'Account', 'HookParameters', 'Memos']
if (flagsOptions.length) {
richFields.push('Flags')
}
const otherFields = Object.keys(txFields).filter(k => !richFields.includes(k)) as [keyof TxFields]
const amountOptions = [
{ label: 'XAH', value: 'xah' },
{ label: 'Token', value: 'token' }
] as const
const defaultTokenAmount = {
value: '0',
currency: '',
issuer: ''
}
return ( return (
<Container <Container
css={{ css={{
p: "$3 01", p: '$3 01',
fontSize: "$sm", fontSize: '$sm',
height: "calc(100% - 45px)", height: 'calc(100% - 45px)'
}} }}
> >
<Flex column fluid css={{ height: "100%", overflowY: "auto" }}> <Flex column fluid css={{ height: '100%', overflowY: 'auto', pr: '$1' }}>
<Flex <TxField label="Transaction type">
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
mt: "1px",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Transaction type:{" "}
</Text>
<Select <Select
instanceId="transactionsType" instanceId="transactionsType"
placeholder="Select transaction type" placeholder="Select transaction type"
options={transactionsOptions} options={transactionsOptions}
hideSelectedOptions hideSelectedOptions
css={{ width: "70%" }}
value={selectedTransaction} value={selectedTransaction}
onChange={(tt: any) => handleChangeTxType(tt)} onChange={(tt: any) => handleChangeTxType(tt)}
/> />
</Flex> </TxField>
<Flex <TxField label="Account">
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Account:{" "}
</Text>
<Select <Select
instanceId="from-account" instanceId="from-account"
placeholder="Select your account" placeholder="Select your account"
css={{ width: "70%" }}
options={accountOptions} options={accountOptions}
value={selectedAccount} value={selectedAccount}
onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc
/> />
</Flex> </TxField>
{txFields.Destination !== undefined && ( <TxField label="Sequence">
<Flex <AccountSequence address={selectedAccount?.value} />
row </TxField>
fluid {richFields.includes('Flags') && (
css={{ <TxField label="Flags">
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Destination account:{" "}
</Text>
<Select <Select
instanceId="to-account"
placeholder="Select the destination account"
css={{ width: "70%" }}
options={destAccountOptions}
value={selectedDestAccount}
isClearable isClearable
onChange={(acc: any) => setState({ selectedDestAccount: acc })} 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
}
/> />
</Flex> </TxField>
)} )}
{otherFields.map(field => { {otherFields.map(field => {
let _value = txFields[field]; let _value = txFields[field]
let value: string | undefined; let value: string | undefined
if (typeof _value === "object") { if (typeIs(_value, 'object')) {
if (_value.$type === "json" && typeof _value.$value === "object") { if (_value.$type === 'json' && typeIs(_value.$value, ['object', 'array'])) {
value = JSON.stringify(_value.$value); value = JSON.stringify(_value.$value, null, 2)
} else { } else {
value = _value.$value.toString(); value = _value.$value?.toString()
} }
} else { } else {
value = _value?.toString(); value = _value?.toString()
} }
let isXrp = typeof _value === "object" && _value.$type === "xrp"; const isAccount = typeIs(_value, 'object') && _value.$type === 'account'
const isXrpAmount = typeIs(_value, 'object') && _value.$type === 'amount.xrp'
const isTokenAmount = typeIs(_value, 'object') && _value.$type === 'amount.token'
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
let tokenAmount = defaultTokenAmount
if (isTokenAmount && typeIs(_value, 'object') && typeIs(_value.$value, 'object')) {
tokenAmount = {
value: _value.$value.value,
currency: _value.$value.currency,
issuer: _value.$value.issuer
}
}
if (isXrpAmount || isTokenAmount) {
return (
<TxField key={field} label={field}>
<Flex fluid css={{ alignItems: 'center' }}>
{isTokenAmount ? (
<Flex
fluid
row
align="center"
justify="space-between"
css={{ position: 'relative' }}
>
{/* <Input
type="text"
placeholder="Issuer"
value={tokenAmount.issuer}
onChange={e =>
setRawField(field, 'amount.token', {
...tokenAmount,
issuer: e.target.value
})
}
/> */}
<Input
type="text"
value={tokenAmount.currency}
placeholder="Currency"
onChange={e => {
setRawField(field, 'amount.token', {
...tokenAmount,
currency: e.target.value
})
}}
/>
<Input
css={{ mx: '$1' }}
type="number"
value={tokenAmount.value}
placeholder="Value"
onChange={e => {
setRawField(field, 'amount.token', {
...tokenAmount,
value: e.target.value
})
}}
/>
<Box css={{ width: '50%' }}>
<CreatableAccount
value={tokenAmount.issuer}
field={'Issuer' as any}
placeholder="Issuer"
setField={(_, value = '') => {
setRawField(field, 'amount.token', {
...tokenAmount,
issuer: value
})
}}
/>
</Box>
</Flex>
) : (
<Input
css={{ flex: 'inherit' }}
type="number"
value={value}
onChange={e => handleSetField(field, e.target.value)}
/>
)}
<Box
css={{
ml: '$2',
width: '150px'
}}
>
<Select
instanceId="currency-type"
options={amountOptions}
value={isXrpAmount ? amountOptions['0'] : amountOptions['1']}
onChange={(e: any) => {
const opt = e as typeof amountOptions[number]
if (opt.value === 'xah') {
setRawField(field, 'amount.xrp', '0')
} else {
setRawField(field, 'amount.token', defaultTokenAmount)
}
}}
/>
</Box>
</Flex>
</TxField>
)
}
if (isAccount) {
return (
<TxField key={field} label={field}>
<CreatableAccount value={value} field={field} setField={handleSetField} />
</TxField>
)
}
return ( return (
<Flex <TxField key={field} label={field}>
key={field} {isJson ? (
row <Textarea
fluid rows={rows}
css={{ value={value}
justifyContent: "flex-end", spellCheck={false}
alignItems: "center", onChange={switchToJson}
mb: "$3", css={{
pr: "1px", 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 (hex-quoted)"
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: '' } }
})
}} }}
> >
<Text muted css={{ mr: "$3" }}> <Plus size="16px" />
{field + (isXrp ? " (XRP)" : "")}:{" "} Add Hook Parameter
</Text> </Button>
<Input </Flex>
value={value} </TxField>
onChange={e => { <TxField multiLine label="Memos">
setState({ <Flex column fluid>
txFields: { {Object.entries(memos).map(([id, memo]) => (
...txFields, <Flex column key={id} css={{ mb: '$2' }}>
[field]: <Flex
typeof _value === "object" row
? { ..._value, $value: e.target.value } css={{
: e.target.value, flexWrap: 'wrap',
}, width: '100%'
}); }}
}} >
css={{ width: "70%", flex: "inherit" }} <Input
/> placeholder="Memo type"
</Flex> value={memo.type}
); onChange={e => {
})} setState({
memos: {
...memos,
[id]: { ...memo, type: e.target.value }
}
})
}}
/>
<Input
placeholder="Data (hex-quoted)"
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> </Flex>
</Container> </Container>
); )
}; }
export const CreatableAccount: FC<{
value: string | undefined
field: keyof TxFields
placeholder?: string
setField: (field: keyof TxFields, value: string, opFields?: TxFields) => void
}> = ({ value, field, setField, placeholder }) => {
const { accounts } = useSnapshot(state)
const accountOptions: SelectOption[] = accounts.map(acc => ({
label: acc.name,
value: acc.address
}))
const label = accountOptions.find(a => a.value === value)?.label || value
const val = {
value,
label
}
placeholder = placeholder || `${capitalize(field)} account`
return (
<CreatableSelect
isClearable
instanceId={field}
placeholder={placeholder}
options={accountOptions}
value={value ? val : undefined}
onChange={(acc: any) => setField(field, acc?.value)}
/>
)
}
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: '$2',
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

@@ -1,11 +1,5 @@
const Carbon = () => ( const Carbon = () => (
<svg <svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
width="66"
height="32"
viewBox="0 0 66 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M33 2L23 15H28L21 24H45L38 15H43L33 2Z" d="M33 2L23 15H28L21 24H45L38 15H43L33 2Z"
stroke="#EDEDEF" stroke="#EDEDEF"
@@ -35,6 +29,6 @@ const Carbon = () => (
fill="#EDEDEF" fill="#EDEDEF"
/> />
</svg> </svg>
); )
export default Carbon; export default Carbon

View File

@@ -1,11 +1,5 @@
const Firewall = () => ( const Firewall = () => (
<svg <svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
width="66"
height="32"
viewBox="0 0 66 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M33 13V7" d="M33 13V7"
stroke="#EDEDEF" stroke="#EDEDEF"
@@ -70,6 +64,6 @@ const Firewall = () => (
fill="#EDEDEF" fill="#EDEDEF"
/> />
</svg> </svg>
); )
export default Firewall; export default Firewall

View File

@@ -1,11 +1,5 @@
const Notary = () => ( const Notary = () => (
<svg <svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
width="66"
height="32"
viewBox="0 0 66 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M37.5 10.5L26.5 21.5L21 16.0002" d="M37.5 10.5L26.5 21.5L21 16.0002"
stroke="#EDEDEF" stroke="#EDEDEF"
@@ -35,6 +29,6 @@ const Notary = () => (
fill="#EDEDEF" fill="#EDEDEF"
/> />
</svg> </svg>
); )
export default Notary; export default Notary

View File

@@ -1,11 +1,5 @@
const Peggy = () => ( const Peggy = () => (
<svg <svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
width="66"
height="32"
viewBox="0 0 66 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <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" 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" stroke="#EDEDEF"
@@ -56,6 +50,6 @@ const Peggy = () => (
fill="#EDEDEF" fill="#EDEDEF"
/> />
</svg> </svg>
); )
export default Peggy; export default Peggy

View File

@@ -1,11 +1,5 @@
const Starter = () => ( const Starter = () => (
<svg <svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
width="66"
height="32"
viewBox="0 0 66 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <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" 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" stroke="#EDEDEF"
@@ -35,6 +29,6 @@ const Starter = () => (
fill="#EDEDEF" fill="#EDEDEF"
/> />
</svg> </svg>
); )
export default Starter; export default Starter

View File

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

View File

@@ -1,14 +1,11 @@
{ {
"uri": "file:///amount-schema.json", "uri": "file:///amount-schema.json",
"title": "Amount", "title": "Amount",
"description": "Specify xrp in drops and tokens as objects.", "description": "Specify XAH in drops and tokens as objects.",
"schema": { "schema": {
"anyOf": [ "anyOf": [
{ {
"type": [ "type": ["number", "string"],
"number",
"string"
],
"exclusiveMinimum": 0, "exclusiveMinimum": 0,
"maximum": "100000000000000000" "maximum": "100000000000000000"
}, },
@@ -16,13 +13,10 @@
"type": "object", "type": "object",
"properties": { "properties": {
"currency": { "currency": {
"description": "Arbitrary currency code for the token. Cannot be XRP." "description": "Arbitrary currency code for the token. Cannot be XAH."
}, },
"value": { "value": {
"type": [ "type": ["string", "number"],
"string",
"number"
],
"description": "Quoted decimal representation of the amount of the token." "description": "Quoted decimal representation of the amount of the token."
}, },
"issuer": { "issuer": {
@@ -34,17 +28,17 @@
], ],
"defaultSnippets": [ "defaultSnippets": [
{ {
"label": "Xrp", "label": "XAH",
"body": "1000000" "body": "1000000"
}, },
{ {
"label": "Token", "label": "Token",
"body": { "body": {
"currency": "${1:13.1}", "currency": "${1:USD}",
"value": "${2:FOO}", "value": "${2:100}",
"description": "${3:rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpns}" "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"
}
]

View File

@@ -2,11 +2,14 @@
{ {
"TransactionType": "AccountDelete", "TransactionType": "AccountDelete",
"Account": "rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm", "Account": "rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm",
"Destination": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe", "Destination": {
"$type": "account",
"$value": ""
},
"DestinationTag": 13, "DestinationTag": 13,
"Fee": "2000000", "Fee": "2000000",
"Sequence": 2470665, "Sequence": 2470665,
"Flags": 2147483648 "Flags": "2147483648"
}, },
{ {
"TransactionType": "AccountSet", "TransactionType": "AccountSet",
@@ -28,7 +31,11 @@
"TransactionType": "CheckCash", "TransactionType": "CheckCash",
"Amount": { "Amount": {
"$value": "100", "$value": "100",
"$type": "xrp" "$type": "amount.xrp"
},
"DeliverMin": {
"$value": "",
"$type": "amount.xrp"
}, },
"CheckID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334", "CheckID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334",
"Fee": "12" "Fee": "12"
@@ -36,7 +43,10 @@
{ {
"TransactionType": "CheckCreate", "TransactionType": "CheckCreate",
"Account": "rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo", "Account": "rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo",
"Destination": "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy", "Destination": {
"$type": "account",
"$value": ""
},
"SendMax": "100000000", "SendMax": "100000000",
"Expiration": 570113521, "Expiration": 570113521,
"InvoiceID": "6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B", "InvoiceID": "6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B",
@@ -48,68 +58,54 @@
"Account": "rsUiUMpnrgxQp24dJYZDhmV4bE3aBtQyt8", "Account": "rsUiUMpnrgxQp24dJYZDhmV4bE3aBtQyt8",
"Authorize": "rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de", "Authorize": "rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de",
"Fee": "10", "Fee": "10",
"Flags": 2147483648, "Flags": "2147483648",
"Sequence": 2 "Sequence": 2
}, },
{ {
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowCancel", "TransactionType": "EscrowCancel",
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Owner": {
"OfferSequence": 7 "$type": "account",
"$value": ""
},
"OfferSequence": 7,
"Fee": "10"
}, },
{ {
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowCreate", "TransactionType": "EscrowCreate",
"Amount": { "Amount": {
"$value": "100", "$value": "100",
"$type": "xrp" "$type": "amount.xrp"
},
"Destination": {
"$type": "account",
"$value": ""
}, },
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"CancelAfter": 533257958, "CancelAfter": 533257958,
"FinishAfter": 533171558, "FinishAfter": 533171558,
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100", "Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
"DestinationTag": 23480, "DestinationTag": 23480,
"SourceTag": 11747 "SourceTag": 11747,
"Fee": "10"
}, },
{ {
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowFinish", "TransactionType": "EscrowFinish",
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Owner": {
"$type": "account",
"$value": ""
},
"OfferSequence": 7, "OfferSequence": 7,
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100", "Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
"Fulfillment": "A0028000" "Fulfillment": "A0028000",
},
{
"TransactionType": "NFTokenBurn",
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
"Fee": "10",
"TokenID": "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65"
},
{
"TransactionType": "NFTokenAcceptOffer",
"Fee": "10" "Fee": "10"
}, },
{
"TransactionType": "NFTokenCancelOffer",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"TokenIDs": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007"
},
{
"TransactionType": "NFTokenCreateOffer",
"Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX",
"TokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
"Amount": {
"$value": "100",
"$type": "xrp"
},
"Flags": 1
},
{ {
"TransactionType": "OfferCancel", "TransactionType": "OfferCancel",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", "Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Fee": "12", "Fee": "12",
"Flags": 0, "Flags": "0",
"LastLedgerSequence": 7108629,
"OfferSequence": 6, "OfferSequence": 6,
"Sequence": 7 "Sequence": 7
}, },
@@ -117,25 +113,34 @@
"TransactionType": "OfferCreate", "TransactionType": "OfferCreate",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", "Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Fee": "12", "Fee": "12",
"Flags": 0, "Flags": "0",
"LastLedgerSequence": 7108682,
"Sequence": 8, "Sequence": 8,
"TakerGets": "6000000", "TakerGets": {
"Amount": { "$type": "amount.xrp",
"$value": "100", "$value": "6000000"
"$type": "xrp" },
"TakerPays": {
"$type": "amount.token",
"$value": {
"currency": "USD",
"issuer": "rhQEswwTsjMXk75QL9Dd9RWZAokNHTzJpr",
"value": "2"
}
} }
}, },
{ {
"TransactionType": "Payment", "TransactionType": "Payment",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", "Destination": {
"$type": "account",
"$value": ""
},
"Amount": { "Amount": {
"$value": "100", "$value": "100",
"$type": "xrp" "$type": "amount.xrp"
}, },
"Fee": "12", "Fee": "12",
"Flags": 2147483648, "Flags": "2147483648",
"Sequence": 2 "Sequence": 2
}, },
{ {
@@ -143,14 +148,18 @@
"TransactionType": "PaymentChannelCreate", "TransactionType": "PaymentChannelCreate",
"Amount": { "Amount": {
"$value": "100", "$value": "100",
"$type": "xrp" "$type": "amount.xrp"
},
"Destination": {
"$type": "account",
"$value": ""
}, },
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"SettleDelay": 86400, "SettleDelay": 86400,
"PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A", "PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A",
"CancelAfter": 533171558, "CancelAfter": 533171558,
"DestinationTag": 23480, "DestinationTag": 23480,
"SourceTag": 11747 "SourceTag": 11747,
"Fee": "10"
}, },
{ {
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
@@ -158,19 +167,20 @@
"Channel": "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198", "Channel": "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198",
"Amount": { "Amount": {
"$value": "200", "$value": "200",
"$type": "xrp" "$type": "amount.xrp"
}, },
"Expiration": 543171558 "Expiration": 543171558,
"Fee": "10"
}, },
{ {
"Flags": 0, "Flags": "0",
"TransactionType": "SetRegularKey", "TransactionType": "SetRegularKey",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "12", "Fee": "12",
"RegularKey": "rAR8rR8sUkBoCZFawhkWzY4Y5YoyuznwD" "RegularKey": "rAR8rR8sUkBoCZFawhkWzY4Y5YoyuznwD"
}, },
{ {
"Flags": 0, "Flags": "0",
"TransactionType": "SignerListSet", "TransactionType": "SignerListSet",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "12", "Fee": "12",
@@ -210,12 +220,67 @@
"TransactionType": "TrustSet", "TransactionType": "TrustSet",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", "Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Fee": "12", "Fee": "12",
"Flags": 262144, "Flags": "262144",
"LastLedgerSequence": 8007750, "LimitAmount": {
"Amount": { "$type": "amount.token",
"$value": "100", "$value": {
"$type": "xrp" "currency": "USD",
"issuer": "rhQEswwTsjMXk75QL9Dd9RWZAokNHTzJpr",
"value": "100"
}
}, },
"Sequence": 12 "Sequence": 12
},
{
"TransactionType": "Invoke",
"Destination": {
"$type": "account",
"$value": ""
},
"Fee": "12"
},
{
"TransactionType": "URITokenMint",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"URI": "697066733A2F2F434944",
"Fee": "10",
"Sequence": 1
},
{
"TransactionType": "URITokenBurn",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"URITokenID": "B792B56B558C89C4E942E41B5DB66074E005CB198DB70C26C707AAC2FF5F74CB",
"Fee": "10",
"Sequence": 1
},
{
"TransactionType": "URITokenCreateSellOffer",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"URITokenID": "B792B56B558C89C4E942E41B5DB66074E005CB198DB70C26C707AAC2FF5F74CB",
"Destination": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Amount": {
"$value": "100",
"$type": "amount.xrp"
},
"Fee": "10",
"Sequence": 1
},
{
"TransactionType": "URITokenCancelSellOffer",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"URITokenID": "B792B56B558C89C4E942E41B5DB66074E005CB198DB70C26C707AAC2FF5F74CB",
"Fee": "10",
"Sequence": 1
},
{
"TransactionType": "URITokenBuy",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"URITokenID": "B792B56B558C89C4E942E41B5DB66074E005CB198DB70C26C707AAC2FF5F74CB",
"Amount": {
"$value": "100",
"$type": "amount.xrp"
},
"Fee": "10",
"Sequence": 1
} }
] ]

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

View File

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

12057
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,24 +7,28 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"postinstall": "patch-package" "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": { "dependencies": {
"@codingame/monaco-jsonrpc": "^0.3.1", "@codingame/monaco-jsonrpc": "^0.3.1",
"@codingame/monaco-languageclient": "^0.17.0", "@codingame/monaco-languageclient": "^0.17.0",
"@monaco-editor/react": "^4.4.1", "@monaco-editor/react": "^4.4.5",
"@octokit/core": "^3.5.1", "@octokit/core": "^3.5.1",
"@radix-ui/colors": "^0.1.7", "@radix-ui/colors": "^0.1.7",
"@radix-ui/react-alert-dialog": "^0.1.1", "@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-dialog": "^0.1.1",
"@radix-ui/react-dropdown-menu": "^0.1.1", "@radix-ui/react-dropdown-menu": "^0.1.6",
"@radix-ui/react-id": "^0.1.1", "@radix-ui/react-id": "^0.1.1",
"@radix-ui/react-label": "^0.1.5", "@radix-ui/react-label": "^0.1.5",
"@radix-ui/react-popover": "^0.1.6", "@radix-ui/react-popover": "^0.1.6",
"@radix-ui/react-switch": "^0.1.5", "@radix-ui/react-switch": "^0.1.5",
"@radix-ui/react-tooltip": "^0.1.7", "@radix-ui/react-tooltip": "^0.1.7",
"@stitches/react": "^1.2.6-0", "@stitches/react": "^1.2.8",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"comment-parser": "^1.3.1",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"filesize": "^8.0.7", "filesize": "^8.0.7",
@@ -34,7 +38,8 @@
"lodash.xor": "^4.5.0", "lodash.xor": "^4.5.0",
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"next": "^12.0.4", "next": "^12.0.4",
"next-auth": "^4.0.0-beta.5", "next-auth": "^4.24.11",
"next-plausible": "^3.2.0",
"next-themes": "^0.1.1", "next-themes": "^0.1.1",
"normalize-url": "^7.0.2", "normalize-url": "^7.0.2",
"octokit": "^1.7.0", "octokit": "^1.7.0",
@@ -42,12 +47,14 @@
"patch-package": "^6.4.7", "patch-package": "^6.4.7",
"phosphor-react": "^1.3.1", "phosphor-react": "^1.3.1",
"postinstall-postinstall": "^2.1.0", "postinstall-postinstall": "^2.1.0",
"prettier": "^2.7.1",
"re-resizable": "^6.9.1", "re-resizable": "^6.9.1",
"react": "17.0.2", "react": "17.0.2",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-hook-form": "^7.28.0", "react-hook-form": "^7.28.0",
"react-hot-keys": "^2.7.1", "react-hot-keys": "^2.7.1",
"react-hot-toast": "^2.1.1", "react-hot-toast": "^2.1.1",
"react-markdown": "^8.0.3",
"react-new-window": "^0.2.1", "react-new-window": "^0.2.1",
"react-select": "^5.2.1", "react-select": "^5.2.1",
"react-split": "^2.0.14", "react-split": "^2.0.14",
@@ -58,9 +65,9 @@
"valtio": "^1.2.5", "valtio": "^1.2.5",
"vscode-languageserver": "^7.0.0", "vscode-languageserver": "^7.0.0",
"vscode-uri": "^3.0.2", "vscode-uri": "^3.0.2",
"wabt": "1.0.16", "wabt": "^1.0.30",
"xrpl-accountlib": "^1.3.2", "xrpl-accountlib": "^1.6.1",
"xrpl-client": "^1.9.4" "xrpl-client": "^2.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/dinero.js": "^1.9.0", "@types/dinero.js": "^1.9.0",
@@ -69,9 +76,16 @@
"@types/lodash.xor": "^4.5.6", "@types/lodash.xor": "^4.5.6",
"@types/pako": "^1.0.2", "@types/pako": "^1.0.2",
"@types/react": "17.0.31", "@types/react": "17.0.31",
"browserify": "^17.0.0",
"eslint": "7.32.0", "eslint": "7.32.0",
"eslint-config-next": "11.1.2", "eslint-config-next": "11.1.2",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"typescript": "4.4.4" "typescript": "^4.9.5"
},
"resolutions": {
"ripple-binary-codec": "=1.6.0"
},
"engines": {
"node": ">=22.0.0"
} }
} }

View File

@@ -1,57 +1,54 @@
import { useEffect } from "react"; import { useEffect } from 'react'
import "../styles/globals.css"; import '../styles/globals.css'
import type { AppProps } from "next/app"; import type { AppProps } from 'next/app'
import Head from "next/head"; import Head from 'next/head'
import { SessionProvider } from "next-auth/react"; import { SessionProvider } from 'next-auth/react'
import { ThemeProvider } from "next-themes"; import { ThemeProvider } from 'next-themes'
import { Toaster } from "react-hot-toast"; import { Toaster } from 'react-hot-toast'
import { useRouter } from "next/router"; import { useRouter } from 'next/router'
import { IdProvider } from "@radix-ui/react-id"; import { IdProvider } from '@radix-ui/react-id'
import PlausibleProvider from 'next-plausible'
import { darkTheme, css } from "../stitches.config"; import { darkTheme, css } from '../stitches.config'
import Navigation from "../components/Navigation"; import Navigation from '../components/Navigation'
import { fetchFiles } from "../state/actions"; import { fetchFiles } from '../state/actions'
import state from "../state"; import state from '../state'
import TimeAgo from "javascript-time-ago"; import TimeAgo from 'javascript-time-ago'
import en from "javascript-time-ago/locale/en.json"; import en from 'javascript-time-ago/locale/en.json'
import { useSnapshot } from "valtio"; import { useSnapshot } from 'valtio'
import Alert from "../components/AlertDialog"; import Alert from '../components/AlertDialog'
import { Button, Flex } from '../components'
import { ChatCircleText } from 'phosphor-react'
TimeAgo.setDefaultLocale(en.locale); TimeAgo.setDefaultLocale(en.locale)
TimeAgo.addLocale(en); TimeAgo.addLocale(en)
function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) { function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
const router = useRouter(); const router = useRouter()
const slug = router.query?.slug; const slug = router.query?.slug
const gistId = (Array.isArray(slug) && slug[0]) ?? null; const gistId = (Array.isArray(slug) && slug[0]) ?? null
const origin = "https://xrpl-hooks-ide.vercel.app"; // TODO: Change when site is deployed const origin = 'https://xrpl-hooks-ide.vercel.app' // TODO: Change when site is deployed
const shareImg = "/share-image.png"; const shareImg = '/share-image.png'
const snap = useSnapshot(state); const snap = useSnapshot(state)
useEffect(() => { useEffect(() => {
if (gistId && router.isReady) { if (gistId && router.isReady) {
fetchFiles(gistId); fetchFiles(gistId)
} else { } else {
if ( if (
!gistId && !gistId &&
router.isReady && router.isReady &&
!router.pathname.includes("/sign-in") && router.pathname.includes('/develop') &&
!snap.files.length && !snap.files.length &&
!snap.mainModalShowed !snap.mainModalShowed
) { ) {
state.mainModalOpen = true; state.mainModalOpen = true
state.mainModalShowed = true; state.mainModalShowed = true
} }
} }
}, [ }, [gistId, router.isReady, router.pathname, snap.files, snap.mainModalShowed])
gistId,
router.isReady,
router.pathname,
snap.files,
snap.mainModalShowed,
]);
return ( return (
<> <>
@@ -61,59 +58,38 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
<meta name="format-detection" content="telephone=no" /> <meta name="format-detection" content="telephone=no" />
<meta property="og:url" content={`${origin}${router.asPath}`} /> <meta property="og:url" content={`${origin}${router.asPath}`} />
<title>XRPL Hooks Builder</title> <title>Xahau Hooks Builder</title>
<meta property="og:title" content="XRPL Hooks Editor" /> <meta property="og:title" content="Xahau Hooks Builder" />
<meta name="twitter:title" content="XRPL Hooks Editor" /> <meta name="twitter:title" content="Xahau Hooks Builder" />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@xrpllabs" /> <meta name="twitter:site" content="@XRPLF" />
<meta <meta
name="description" name="description"
content="Hooks Builder, add smart contract functionality to the XRP Ledger." content="Hooks Builder, add smart contract functionality to the Xahau Network."
/> />
<meta <meta
property="og:description" property="og:description"
content="Hooks Builder, add smart contract functionality to the XRP Ledger." content="Hooks Builder, add smart contract functionality to the Xahau Network."
/> />
<meta <meta
name="twitter:description" name="twitter:description"
content="Hooks Builder, add smart contract functionality to the XRP Ledger." content="Hooks Builder, add smart contract functionality to the Xahau Network."
/> />
<meta property="og:image" content={`${origin}${shareImg}`} /> <meta property="og:image" content={`${origin}${shareImg}`} />
<meta property="og:image:width" content="1200" /> <meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" /> <meta property="og:image:height" content="630" />
<meta name="twitter:image" content={`${origin}${shareImg}`} /> <meta name="twitter:image" content={`${origin}${shareImg}`} />
<link <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
rel="apple-touch-icon" <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
sizes="180x180" <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
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="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#161618" /> <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#161618" />
<meta name="application-name" content="XRPL Hooks Editor" /> <meta name="application-name" content="XRPL Hooks Builder" />
<meta name="msapplication-TileColor" content="#c10ad0" /> <meta name="msapplication-TileColor" content="#c10ad0" />
<meta <meta name="theme-color" content="#161618" media="(prefers-color-scheme: dark)" />
name="theme-color" <meta name="theme-color" content="#FDFCFD" media="(prefers-color-scheme: light)" />
content="#161618"
media="(prefers-color-scheme: dark)"
/>
<meta
name="theme-color"
content="#FDFCFD"
media="(prefers-color-scheme: light)"
/>
</Head> </Head>
<IdProvider> <IdProvider>
<SessionProvider session={session}> <SessionProvider session={session}>
<ThemeProvider <ThemeProvider
@@ -121,31 +97,45 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
defaultTheme="dark" defaultTheme="dark"
enableSystem={false} enableSystem={false}
value={{ value={{
light: "light", light: 'light',
dark: darkTheme.className, dark: darkTheme.className
}} }}
> >
<Navigation /> <PlausibleProvider domain="hooks-builder.xrpl.org" trackOutboundLinks>
<Component {...pageProps} /> <Navigation />
<Toaster <Component {...pageProps} />
toastOptions={{ <Toaster
className: css({ toastOptions={{
backgroundColor: "$mauve1", className: css({
color: "$mauve10", backgroundColor: '$mauve1',
fontSize: "$sm", color: '$mauve10',
zIndex: 9999, fontSize: '$sm',
".dark &": { zIndex: 9999,
backgroundColor: "$mauve4", '.dark &': {
color: "$mauve12", backgroundColor: '$mauve4',
}, color: '$mauve12'
})(), }
}} })()
/> }}
<Alert /> />
<Alert />
<Flex
as="a"
href="https://github.com/Xahau/xrpl-hooks-ide/issues"
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> </ThemeProvider>
</SessionProvider> </SessionProvider>
</IdProvider> </IdProvider>
</> </>
); )
} }
export default MyApp; export default MyApp

View File

@@ -1,35 +1,22 @@
import Document, { import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/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 { class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) { static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx); const initialProps = await Document.getInitialProps(ctx)
return initialProps; return initialProps
} }
render() { render() {
globalStyles(); globalStyles()
return ( return (
<Html> <Html>
<Head> <Head>
<style <style id="stitches" dangerouslySetInnerHTML={{ __html: getCssText() }} />
id="stitches"
dangerouslySetInnerHTML={{ __html: getCssText() }}
/>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin=""
/>
<link <link
href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital@0;1&family=Work+Sans:wght@400;600;700&display=swap" href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital@0;1&family=Work+Sans:wght@400;600;700&display=swap"
rel="stylesheet" rel="stylesheet"
@@ -40,8 +27,8 @@ class MyDocument extends Document {
<NextScript /> <NextScript />
</body> </body>
</Html> </Html>
); )
} }
} }
export default MyDocument; export default MyDocument

View File

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

View File

@@ -1,4 +1,10 @@
import NextAuth from "next-auth" import NextAuth from 'next-auth'
declare module "next-auth" {
interface User {
username: string
}
}
export default NextAuth({ export default NextAuth({
// Configure one or more authentication providers // Configure one or more authentication providers
@@ -10,39 +16,38 @@ export default NextAuth({
// scope: 'user,gist' // scope: 'user,gist'
// }), // }),
{ {
id: "github", id: 'github',
name: "GitHub", name: 'GitHub',
type: "oauth", type: 'oauth',
clientId: process.env.GITHUB_ID, clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET, clientSecret: process.env.GITHUB_SECRET,
authorization: "https://github.com/login/oauth/authorize?scope=read:user+user:email+gist", authorization: 'https://github.com/login/oauth/authorize?scope=read:user+user:email+gist',
token: "https://github.com/login/oauth/access_token", token: 'https://github.com/login/oauth/access_token',
userinfo: "https://api.github.com/user", userinfo: 'https://api.github.com/user',
profile(profile) { profile(profile) {
return { return {
id: profile.id.toString(), id: profile.id.toString(),
name: profile.name || profile.login, name: profile.name || profile.login,
username: profile.login, username: profile.login,
email: profile.email, email: profile.email,
image: profile.avatar_url, image: profile.avatar_url
} }
}, }
} }
// ...add more providers here // ...add more providers here
], ],
callbacks: { callbacks: {
async jwt({ token, user, account, profile, isNewUser }) { async jwt({ token, user, account, profile, isNewUser }) {
if (account && account.access_token) { if (account && account.access_token) {
token.accessToken = account.access_token; token.accessToken = account.access_token
token.username = user?.username || ''; token.username = user?.username || ''
} }
return token return token
}, },
async session({ session, token }) { async session({ session, token }) {
session.accessToken = token.accessToken as string; session.accessToken = token.accessToken as string
session['user']['username'] = token.username as string; session['user']['username'] = token.username as string
return session return session
} }
}, }
}) })

View File

@@ -6,14 +6,13 @@ interface ErrorResponse {
} }
export interface Faucet { export interface Faucet {
address: string; address: string
secret: string; secret: string
xrp: number; xrp: number
hash: string; hash: string
code: string; code: string
} }
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse<Faucet | ErrorResponse> res: NextApiResponse<Faucet | ErrorResponse>
@@ -21,20 +20,25 @@ export default async function handler(
if (req.method !== 'POST') { if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed!' }) return res.status(405).json({ error: 'Method not allowed!' })
} }
const { account } = req.query; const { account } = req.query
const ip = Array.isArray(req?.headers?.["x-real-ip"]) ? req?.headers?.["x-real-ip"][0] : req?.headers?.["x-real-ip"]; const ip = Array.isArray(req?.headers?.['x-real-ip'])
? req?.headers?.['x-real-ip'][0]
: req?.headers?.['x-real-ip']
try { try {
const response = await fetch(`https://${process.env.NEXT_PUBLIC_TESTNET_URL}/newcreds?account=${account ? account : ''}`, { const response = await fetch(
method: 'POST', `https://${process.env.NEXT_PUBLIC_TESTNET_URL}/newcreds?account=${account ? account : ''}`,
headers: { {
'x-forwarded-for': ip || '', method: 'POST',
}, headers: {
}); 'x-forwarded-for': ip || ''
const json: Faucet | ErrorResponse = await response.json(); }
if ("error" in json) { }
)
const json: Faucet | ErrorResponse = await response.json()
if ('error' in json) {
return res.status(429).json(json) return res.status(429).json(json)
} }
return res.status(200).json(json); return res.status(200).json(json)
} catch (err) { } catch (err) {
console.log(err) console.log(err)
return res.status(500).json({ error: 'Server error' }) return res.status(500).json({ error: 'Server error' })

View File

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

View File

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

View File

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

View File

@@ -1,34 +1,34 @@
import { Label } from "@radix-ui/react-label"; import { Label } from '@radix-ui/react-label'
import { Switch, SwitchThumb } from "../../components/Switch"; import type { NextPage } from 'next'
import type { NextPage } from "next"; import dynamic from 'next/dynamic'
import dynamic from "next/dynamic"; import { FileJs, Gear, Play } from 'phosphor-react'
import { Gear, Play } from "phosphor-react"; import Hotkeys from 'react-hot-keys'
import Hotkeys from "react-hot-keys"; import Split from 'react-split'
import Split from "react-split"; import { useSnapshot } from 'valtio'
import { useSnapshot } from "valtio"; import { ButtonGroup, Flex } from '../../components'
import { ButtonGroup, Flex } from "../../components"; import Box from '../../components/Box'
import Box from "../../components/Box"; import Button from '../../components/Button'
import Button from "../../components/Button"; import Popover from '../../components/Popover'
import Popover from "../../components/Popover"; import RunScript from '../../components/RunScript'
import state from "../../state"; import state, { IFile } from '../../state'
import { compileCode } from "../../state/actions"; import { compileCode } from '../../state/actions'
import { getSplit, saveSplit } from "../../state/actions/persistSplits"; import { getSplit, saveSplit } from '../../state/actions/persistSplits'
import { styled } from "../../stitches.config"; import { styled } from '../../stitches.config'
import { getFileExtention } from '../../utils/helpers'
const HooksEditor = dynamic(() => import("../../components/HooksEditor"), { const HooksEditor = dynamic(() => import('../../components/HooksEditor'), {
ssr: false, ssr: false
}); })
const LogBox = dynamic(() => import("../../components/LogBox"), { const LogBox = dynamic(() => import('../../components/LogBox'), {
ssr: false, ssr: false
}); })
const OptimizationText = () => ( const OptimizationText = () => (
<span> <span>
Specify which optimization level to use for compiling. For example -O0 means Specify which optimization level to use for compiling. For example -O0 means no optimization:
no optimization: this level compiles the fastest and generates the most this level compiles the fastest and generates the most debuggable code. -O2 means moderate level
debuggable code. -O2 means moderate level of optimization which enables most of optimization which enables most optimizations. Read more about the options from{' '}
optimizations. Read more about the options from{" "}
<a <a
className="link" className="link"
rel="noopener noreferrer" rel="noopener noreferrer"
@@ -39,197 +39,144 @@ const OptimizationText = () => (
</a> </a>
. .
</span> </span>
); )
const StyledOptimizationText = styled(OptimizationText, { const StyledOptimizationText = styled(OptimizationText, {
color: "$mauve12 !important", color: '$mauve12 !important',
fontSize: "200px", fontSize: '200px',
"span a.link": { 'span a.link': {
color: "red", color: 'red'
}, }
}); })
const CompilerSettings = () => { const CompilerSettings = () => {
const snap = useSnapshot(state); const snap = useSnapshot(state)
return ( return (
<Flex css={{ minWidth: 200, flexDirection: "column", gap: "$5" }}> <Flex css={{ minWidth: 200, flexDirection: 'column', gap: '$5' }}>
<Box> <Box>
<Label <Label
style={{ style={{
flexDirection: "row", flexDirection: 'row',
display: "flex", display: 'flex'
}} }}
> >
Optimization level{" "} Optimization level{' '}
<Popover <Popover
css={{ css={{
maxWidth: "240px", maxWidth: '240px',
lineHeight: "1.3", lineHeight: '1.3',
a: { a: {
color: "$purple11", color: '$purple11'
}, },
".dark &": { '.dark &': {
backgroundColor: "$black !important", backgroundColor: '$black !important',
".arrow": { '.arrow': {
fill: "$colors$black", fill: '$colors$black'
}, }
}, }
}} }}
content={<StyledOptimizationText />} content={<StyledOptimizationText />}
> >
<Flex <Flex
css={{ css={{
position: "relative", position: 'relative',
top: "-1px", top: '-1px',
ml: "$1", ml: '$1',
backgroundColor: "$mauve8", backgroundColor: '$mauve8',
borderRadius: "$full", borderRadius: '$full',
cursor: "pointer", cursor: 'pointer',
width: "16px", width: '16px',
height: "16px", height: '16px',
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center'
}} }}
> >
? ?
</Flex> </Flex>
</Popover> </Popover>
</Label> </Label>
<ButtonGroup css={{ mt: "$2", fontFamily: "$monospace" }}> <ButtonGroup css={{ mt: '$2', fontFamily: '$monospace' }}>
<Button <Button
css={{ fontFamily: "$monospace" }} css={{ fontFamily: '$monospace' }}
outline={snap.compileOptions.optimizationLevel !== "-O0"} outline={snap.compileOptions.optimizationLevel !== '-O0'}
onClick={() => (state.compileOptions.optimizationLevel = "-O0")} onClick={() => (state.compileOptions.optimizationLevel = '-O0')}
> >
-O0 -O0
</Button> </Button>
<Button <Button
css={{ fontFamily: "$monospace" }} css={{ fontFamily: '$monospace' }}
outline={snap.compileOptions.optimizationLevel !== "-O1"} outline={snap.compileOptions.optimizationLevel !== '-O1'}
onClick={() => (state.compileOptions.optimizationLevel = "-O1")} onClick={() => (state.compileOptions.optimizationLevel = '-O1')}
> >
-O1 -O1
</Button> </Button>
<Button <Button
css={{ fontFamily: "$monospace" }} css={{ fontFamily: '$monospace' }}
outline={snap.compileOptions.optimizationLevel !== "-O2"} outline={snap.compileOptions.optimizationLevel !== '-O2'}
onClick={() => (state.compileOptions.optimizationLevel = "-O2")} onClick={() => (state.compileOptions.optimizationLevel = '-O2')}
> >
-O2 -O2
</Button> </Button>
<Button <Button
css={{ fontFamily: "$monospace" }} css={{ fontFamily: '$monospace' }}
outline={snap.compileOptions.optimizationLevel !== "-O3"} outline={snap.compileOptions.optimizationLevel !== '-O3'}
onClick={() => (state.compileOptions.optimizationLevel = "-O3")} onClick={() => (state.compileOptions.optimizationLevel = '-O3')}
> >
-O3 -O3
</Button> </Button>
<Button <Button
css={{ fontFamily: "$monospace" }} css={{ fontFamily: '$monospace' }}
outline={snap.compileOptions.optimizationLevel !== "-O4"} outline={snap.compileOptions.optimizationLevel !== '-O4'}
onClick={() => (state.compileOptions.optimizationLevel = "-O4")} onClick={() => (state.compileOptions.optimizationLevel = '-O4')}
> >
-O4 -O4
</Button> </Button>
<Button <Button
css={{ fontFamily: "$monospace" }} css={{ fontFamily: '$monospace' }}
outline={snap.compileOptions.optimizationLevel !== "-Os"} outline={snap.compileOptions.optimizationLevel !== '-Os'}
onClick={() => (state.compileOptions.optimizationLevel = "-Os")} onClick={() => (state.compileOptions.optimizationLevel = '-Os')}
> >
-Os -Os
</Button> </Button>
</ButtonGroup> </ButtonGroup>
</Box> </Box>
<Box css={{ flexDirection: "column" }}>
<Label
style={{
flexDirection: "row",
display: "flex",
}}
>
Clean WASM (experimental){" "}
<Popover
css={{
maxWidth: "240px",
lineHeight: "1.3",
a: {
color: "$purple11",
},
".dark &": {
backgroundColor: "$black !important",
".arrow": {
fill: "$colors$black",
},
},
}}
content="Cleaner removes unwanted compiler-provided exports and functions from a wasm binary to make it (more) suitable for being used as a Hook"
>
<Flex
css={{
position: "relative",
top: "-1px",
mx: "$1",
backgroundColor: "$mauve8",
borderRadius: "$full",
cursor: "pointer",
width: "16px",
height: "16px",
alignItems: "center",
justifyContent: "center",
}}
>
?
</Flex>
</Popover>
</Label>
<Switch
css={{ mt: "$2" }}
checked={snap.compileOptions.strip}
onCheckedChange={(checked) => {
state.compileOptions.strip = checked;
}}
>
<SwitchThumb />
</Switch>
</Box>
</Flex> </Flex>
); )
}; }
const Home: NextPage = () => { const Home: NextPage = () => {
const snap = useSnapshot(state); 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 ( return (
<Split <Split
direction="vertical" direction="vertical"
sizes={getSplit("developVertical") || [70, 30]} sizes={getSplit('developVertical') || [70, 30]}
minSize={[100, 100]} minSize={[100, 100]}
gutterAlign="center" gutterAlign="center"
gutterSize={4} gutterSize={4}
style={{ height: "calc(100vh - 60px)" }} style={{ height: 'calc(100vh - 60px)' }}
onDragEnd={(e) => saveSplit("developVertical", e)} onDragEnd={e => saveSplit('developVertical', e)}
> >
<main style={{ display: "flex", flex: 1, position: "relative" }}> <main style={{ display: 'flex', flex: 1, position: 'relative' }}>
<HooksEditor /> <HooksEditor />
{snap.files[snap.active]?.name?.split(".")?.[1].toLowerCase() === {canCompile && (
"c" && (
<Hotkeys <Hotkeys
keyName="command+b,ctrl+b" keyName="command+b,ctrl+b"
onKeyDown={() => onKeyDown={() => !snap.compiling && snap.files.length && compileCode(snap.active)}
!snap.compiling && snap.files.length && compileCode(snap.active)
}
> >
<Flex <Flex
css={{ css={{
position: "absolute", position: 'absolute',
bottom: "$4", bottom: '$4',
left: "$4", left: '$4',
alignItems: "center", alignItems: 'center',
display: "flex", display: 'flex',
cursor: "pointer", cursor: 'pointer',
gap: "$2", gap: '$2'
}} }}
> >
<Button <Button
@@ -243,29 +190,62 @@ const Home: NextPage = () => {
Compile to Wasm Compile to Wasm
</Button> </Button>
<Popover content={<CompilerSettings />}> <Popover content={<CompilerSettings />}>
<Button variant="primary" css={{ px: "10px" }}> <Button variant="primary" css={{ px: '10px' }}>
<Gear size="16px" /> <Gear size="16px" />
</Button> </Button>
</Popover> </Popover>
</Flex> </Flex>
</Hotkeys> </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> </main>
<Box <Flex css={{ width: '100%' }}>
css={{ <Flex
display: "flex", css={{
background: "$mauve1", flex: 1,
position: "relative", background: '$mauve1',
}} position: 'relative',
> borderRight: '1px solid $mauve8'
<LogBox }}
title="Development Log" >
clearLog={() => (state.logs = [])} <LogBox title="Development Log" clearLog={() => (state.logs = [])} logs={snap.logs} />
logs={snap.logs} </Flex>
/> {activeFileExt === 'js' && (
</Box> <Flex
css={{
flex: 1
}}
>
<LogBox
Icon={FileJs}
title="Script Log"
logs={snap.scriptLogs}
clearLog={() => (state.scriptLogs = [])}
/>
</Flex>
)}
</Flex>
</Split> </Split>
); )
}; }
export default Home; export default Home

View File

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

View File

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

View File

@@ -1,91 +1,134 @@
import dynamic from "next/dynamic"; import dynamic from 'next/dynamic'
import Split from "react-split"; import Split from 'react-split'
import { useSnapshot } from "valtio"; import { useSnapshot } from 'valtio'
import { Box, Container, Flex, Tab, Tabs } from "../../components"; import { Box, Container, Flex, Tab, Tabs } from '../../components'
import Transaction from "../../components/Transaction"; import Transaction from '../../components/Transaction'
import state from "../../state"; import state, { renameTxState } from '../../state'
import { getSplit, saveSplit } from "../../state/actions/persistSplits"; import { getSplit, saveSplit } from '../../state/actions/persistSplits'
import { transactionsState, modifyTransaction } from "../../state"; 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"), { const DebugStream = dynamic(() => import('../../components/DebugStream'), {
ssr: false, ssr: false
}); })
const LogBox = dynamic(() => import("../../components/LogBox"), { const LogBox = dynamic(() => import('../../components/LogBox'), {
ssr: false, ssr: false
}); })
const Accounts = dynamic(() => import("../../components/Accounts"), { const Accounts = dynamic(() => import('../../components/Accounts'), {
ssr: false, ssr: false
}); })
const Test = () => { const Test = () => {
const { transactionLogs } = useSnapshot(state); // This and useEffect is here to prevent useLayoutEffect warnings from react-split
const { transactions, activeHeader } = useSnapshot(transactionsState); 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 ( return (
<Container css={{ px: 0 }}> <Container css={{ px: 0 }}>
<Split <Split
direction="vertical" direction="vertical"
sizes={getSplit("testVertical") || [50, 50]} sizes={
hasScripts && getSplit('testVertical')?.length === 2
? [50, 20, 30]
: hasScripts
? [50, 20, 50]
: [50, 50]
}
gutterSize={4} gutterSize={4}
gutterAlign="center" gutterAlign="center"
style={{ height: "calc(100vh - 60px)" }} style={{ height: 'calc(100vh - 60px)' }}
onDragEnd={e => saveSplit("testVertical", e)} onDragEnd={e => saveSplit('testVertical', e)}
> >
<Flex <Flex
row row
fluid fluid
css={{ css={{
justifyContent: "center", justifyContent: 'center',
p: "$3 $2", p: '$3 $2'
}} }}
> >
<Split <Split
direction="horizontal" direction="horizontal"
sizes={getSplit("testHorizontal") || [50, 50]} sizes={getSplit('testHorizontal') || [50, 50]}
minSize={[180, 320]} minSize={[180, 320]}
gutterSize={4} gutterSize={4}
gutterAlign="center" gutterAlign="center"
style={{ style={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
width: "100%", width: '100%',
height: "100%", height: '100%'
}} }}
onDragEnd={e => saveSplit("testHorizontal", e)} onDragEnd={e => saveSplit('testHorizontal', e)}
> >
<Box css={{ width: "55%", px: "$2" }}> <Box css={{ width: '55%', px: '$2' }}>
<Tabs <Tabs
label="Transaction"
activeHeader={activeHeader} activeHeader={activeHeader}
// TODO make header a required field // TODO make header a required field
onChangeActive={(idx, header) => { onChangeActive={(idx, header) => {
if (header) transactionsState.activeHeader = header; if (header) transactionsState.activeHeader = header
}} }}
keepAllAlive keepAllAlive
forceDefaultExtension defaultExtension="json"
defaultExtension=".json" allowedExtensions={['json']}
onCreateNewTab={header => modifyTransaction(header, {})} onCreateNewTab={header => modifyTxState(header, {})}
onCloseTab={(idx, header) => onRenameTab={(idx, nwName, oldName = '') => renameTxState(oldName, nwName)}
header && modifyTransaction(header, undefined) onCloseTab={(idx, header) => header && modifyTxState(header, undefined)}
}
> >
{transactions.map(({ header, state }) => ( {transactions.map(({ header, state }) => (
<Tab key={header} header={header}> <Tab key={header} header={header}>
<Transaction <Transaction state={state} header={header} />
state={state}
header={header}
/>
</Tab> </Tab>
))} ))}
</Tabs> </Tabs>
</Box> </Box>
<Box css={{ width: "45%", mx: "$2", height: "100%" }}> <Box css={{ width: '45%', mx: '$2', height: '100%' }}>
<Accounts card hideDeployBtn showHookStats /> <Accounts card hideDeployBtn showHookStats />
</Box> </Box>
</Split> </Split>
</Flex> </Flex>
{hasScripts ? (
<Flex row fluid> <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 <Split
direction="horizontal" direction="horizontal"
sizes={[50, 50]} sizes={[50, 50]}
@@ -93,16 +136,16 @@ const Test = () => {
gutterSize={4} gutterSize={4}
gutterAlign="center" gutterAlign="center"
style={{ style={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
width: "100%", width: '100%',
height: "100%", height: '100%'
}} }}
> >
<Box <Box
css={{ css={{
borderRight: "1px solid $mauve8", borderRight: '1px solid $mauve8',
height: "100%", height: '100%'
}} }}
> >
<LogBox <LogBox
@@ -111,14 +154,14 @@ const Test = () => {
clearLog={() => (state.transactionLogs = [])} clearLog={() => (state.transactionLogs = [])}
/> />
</Box> </Box>
<Box css={{ height: "100%" }}> <Box css={{ height: '100%' }}>
<DebugStream /> <DebugStream />
</Box> </Box>
</Split> </Split>
</Flex> </Flex>
</Split> </Split>
</Container> </Container>
); )
}; }
export default Test; export default Test

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,346 @@
diff --git a/node_modules/ripple-binary-codec/dist/enums/definitions.json b/node_modules/ripple-binary-codec/dist/enums/definitions.json
index e623376..7e1e4d5 100644
--- a/node_modules/ripple-binary-codec/dist/enums/definitions.json
+++ b/node_modules/ripple-binary-codec/dist/enums/definitions.json
@@ -44,11 +44,16 @@
"NegativeUNL": 78,
"NFTokenPage": 80,
"NFTokenOffer": 55,
+ "URIToken": 85,
"Any": -3,
"Child": -2,
"Nickname": 110,
"Contract": 99,
- "GeneratorMap": 103
+ "GeneratorMap": 103,
+ "Hook": 72,
+ "HookState": 118,
+ "HookDefinition": 68,
+ "EmittedTxn": 69
},
"FIELDS": [
[
@@ -321,6 +326,16 @@
"type": "UInt16"
}
],
+ [
+ "NetworkID",
+ {
+ "nth": 1,
+ "isVLEncoded": false,
+ "isSerialized": true,
+ "isSigningField": true,
+ "type": "UInt32"
+ }
+ ],
[
"Flags",
{
@@ -761,6 +776,36 @@
"type": "UInt32"
}
],
+ [
+ "LockCount",
+ {
+ "nth": 49,
+ "isVLEncoded": false,
+ "isSerialized": true,
+ "isSigningField": true,
+ "type": "UInt32"
+ }
+ ],
+ [
+ "FirstNFTokenSequence",
+ {
+ "nth": 50,
+ "isVLEncoded": false,
+ "isSerialized": true,
+ "isSigningField": true,
+ "type": "UInt32"
+ }
+ ],
+ [
+ "ImportSequence",
+ {
+ "nth": 97,
+ "isVLEncoded": false,
+ "isSerialized": true,
+ "isSigningField": true,
+ "type": "UInt32"
+ }
+ ],
[
"IndexNext",
{
@@ -891,16 +936,6 @@
"type": "UInt64"
}
],
- [
- "HookOn",
- {
- "nth": 16,
- "isVLEncoded": false,
- "isSerialized": true,
- "isSigningField": true,
- "type": "UInt64"
- }
- ],
[
"HookInstructionCount",
{
@@ -1151,6 +1186,16 @@
"type": "Hash256"
}
],
+ [
+ "HookOn",
+ {
+ "nth": 20,
+ "isVLEncoded": false,
+ "isSerialized": true,
+ "isSigningField": true,
+ "type": "Hash256"
+ }
+ ],
[
"Digest",
{
@@ -1281,6 +1326,36 @@
"type": "Hash256"
}
],
+ [
+ "OfferID",
+ {
+ "nth": 34,
+ "isVLEncoded": false,
+ "isSerialized": true,
+ "isSigningField": true,
+ "type": "Hash256"
+ }
+ ],
+ [
+ "EscrowID",
+ {
+ "nth": 35,
+ "isVLEncoded": false,
+ "isSerialized": true,
+ "isSigningField": true,
+ "type": "Hash256"
+ }
+ ],
+ [
+ "URITokenID",
+ {
+ "nth": 36,
+ "isVLEncoded": false,
+ "isSerialized": true,
+ "isSigningField": true,
+ "type": "Hash256"
+ }
+ ],
[
"Amount",
{
@@ -1421,6 +1496,56 @@
"type": "Amount"
}
],
+ [
+ "HookCallbackFee",
+ {
+ "nth": 20,
+ "isVLEncoded": false,
+ "isSerialized": true,
+ "isSigningField": true,
+ "type": "Amount"
+ }
+ ],
+ [
+ "LockedBalance",
+ {
+ "nth": 21,
+ "isVLEncoded": false,
+ "isSerialized": true,
+ "isSigningField": true,
+ "type": "Amount"
+ }
+ ],
+ [
+ "BaseFeeDrops",
+ {
+ "nth": 22,
+ "isVLEncoded": false,
+ "isSerialized": true,
+ "isSigningField": true,
+ "type": "Amount"
+ }
+ ],
+ [
+ "ReserveBaseDrops",
+ {
+ "nth": 23,
+ "isVLEncoded": false,
+ "isSerialized": true,
+ "isSigningField": true,
+ "type": "Amount"
+ }
+ ],
+ [
+ "ReserveIncrementDrops",
+ {
+ "nth": 24,
+ "isVLEncoded": false,
+ "isSerialized": true,
+ "isSigningField": true,
+ "type": "Amount"
+ }
+ ],
[
"PublicKey",
{
@@ -1661,6 +1786,16 @@
"type": "Blob"
}
],
+ [
+ "Blob",
+ {
+ "nth": 26,
+ "isVLEncoded": true,
+ "isSerialized": true,
+ "isSigningField": true,
+ "type": "Blob"
+ }
+ ],
[
"Account",
{
@@ -1801,6 +1936,16 @@
"type": "Vector256"
}
],
+ [
+ "HookNamespaces",
+ {
+ "nth": 5,
+ "isVLEncoded": true,
+ "isSerialized": true,
+ "isSigningField": true,
+ "type": "Vector256"
+ }
+ ],
[
"Paths",
{
@@ -2176,6 +2321,12 @@
"telCAN_NOT_QUEUE_BLOCKED": -389,
"telCAN_NOT_QUEUE_FEE": -388,
"telCAN_NOT_QUEUE_FULL": -387,
+ "telWRONG_NETWORK": -386,
+ "telREQUIRES_NETWORK_ID": -385,
+ "telNETWORK_ID_MAKES_TX_NON_CANONICAL": -384,
+ "telNON_LOCAL_EMITTED_TXN": -383,
+ "telIMPORT_VL_KEY_NOT_RECOGNISED": -382,
+ "telCAN_NOT_QUEUE_IMPORT": -381,
"temMALFORMED": -299,
"temBAD_AMOUNT": -298,
"temBAD_CURRENCY": -297,
@@ -2214,6 +2365,16 @@
"temUNKNOWN": -264,
"temSEQ_AND_TICKET": -263,
"temBAD_NFTOKEN_TRANSFER_FEE": -262,
+ "temAMM_BAD_TOKENS": -261,
+ "temXCHAIN_EQUAL_DOOR_ACCOUNTS": -260,
+ "temXCHAIN_BAD_PROOF": -259,
+ "temXCHAIN_BRIDGE_BAD_ISSUES": -258,
+ "temXCHAIN_BRIDGE_NONDOOR_OWNER": -257,
+ "temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT": -256,
+ "temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT": -255,
+ "temXCHAIN_TOO_MANY_ATTESTATIONS": -254,
+ "temHOOK_DATA_TOO_LARGE": -253,
+ "temHOOK_REJECTED": -252,
"tefFAILURE": -199,
"tefALREADY": -198,
"tefBAD_ADD_AUTH": -197,
@@ -2235,6 +2396,7 @@
"tefTOO_BIG": -181,
"tefNO_TICKET": -180,
"tefNFTOKEN_IS_NOT_TRANSFERABLE": -179,
+ "tefPAST_IMPORT_SEQ": -178,
"terRETRY": -99,
"terFUNDS_SPENT": -98,
"terINSUF_FEE_B": -97,
@@ -2247,6 +2409,8 @@
"terNO_RIPPLE": -90,
"terQUEUED": -89,
"terPRE_TICKET": -88,
+ "terNO_AMM": -87,
+ "terNO_HOOK": -86,
"tesSUCCESS": 0,
"tecCLAIM": 100,
"tecPATH_PARTIAL": 101,
@@ -2286,6 +2450,7 @@
"tecKILLED": 150,
"tecHAS_OBLIGATIONS": 151,
"tecTOO_SOON": 152,
+ "tecHOOK_REJECTED": 153,
"tecMAX_SEQUENCE_REACHED": 154,
"tecNO_SUITABLE_NFTOKEN_PAGE": 155,
"tecNFTOKEN_BUY_SELL_MISMATCH": 156,
@@ -2293,7 +2458,33 @@
"tecCANT_ACCEPT_OWN_NFTOKEN_OFFER": 158,
"tecINSUFFICIENT_FUNDS": 159,
"tecOBJECT_NOT_FOUND": 160,
- "tecINSUFFICIENT_PAYMENT": 161
+ "tecINSUFFICIENT_PAYMENT": 161,
+ "tecAMM_UNFUNDED": 162,
+ "tecAMM_BALANCE": 163,
+ "tecAMM_FAILED_DEPOSIT": 164,
+ "tecAMM_FAILED_WITHDRAW": 165,
+ "tecAMM_INVALID_TOKENS": 166,
+ "tecAMM_FAILED_BID": 167,
+ "tecAMM_FAILED_VOTE": 168,
+ "tecREQUIRES_FLAG": 169,
+ "tecPRECISION_LOSS": 170,
+ "tecBAD_XCHAIN_TRANSFER_ISSUE": 171,
+ "tecXCHAIN_NO_CLAIM_ID": 172,
+ "tecXCHAIN_BAD_CLAIM_ID": 173,
+ "tecXCHAIN_CLAIM_NO_QUORUM": 174,
+ "tecXCHAIN_PROOF_UNKNOWN_KEY": 175,
+ "tecXCHAIN_CREATE_ACCOUNT_NONXRP_ISSUE": 176,
+ "tecXCHAIN_WRONG_CHAIN": 177,
+ "tecXCHAIN_REWARD_MISMATCH": 178,
+ "tecXCHAIN_NO_SIGNERS_LIST": 179,
+ "tecXCHAIN_SENDING_ACCOUNT_MISMATCH": 180,
+ "tecXCHAIN_INSUFF_CREATE_AMOUNT": 181,
+ "tecXCHAIN_ACCOUNT_CREATE_PAST": 182,
+ "tecXCHAIN_ACCOUNT_CREATE_TOO_MANY": 183,
+ "tecXCHAIN_PAYMENT_FAILED": 184,
+ "tecXCHAIN_SELF_COMMIT": 185,
+ "tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR": 186,
+ "tecLAST_POSSIBLE_ENTRY": 255
},
"TRANSACTION_TYPES": {
"Invalid": -1,
@@ -2325,8 +2516,16 @@
"NFTokenCreateOffer": 27,
"NFTokenCancelOffer": 28,
"NFTokenAcceptOffer": 29,
+ "URITokenMint": 45,
+ "URITokenBurn": 46,
+ "URITokenBuy": 47,
+ "URITokenCreateSellOffer": 48,
+ "URITokenCancelSellOffer": 49,
+ "Import": 97,
+ "Invoke": 99,
"EnableAmendment": 100,
"SetFee": 101,
- "UNLModify": 102
+ "UNLModify": 102,
+ "EmitFailure": 103
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 710 KiB

After

Width:  |  Height:  |  Size: 352 KiB

View File

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

13
raw-loader.d.ts vendored
View File

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

View File

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

View File

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

View File

@@ -1,17 +1,29 @@
import state, { IFile } from '../index'; import { getFileExtention } from '../../utils/helpers'
import state, { IFile } from '../index'
const languageMapping: Record<string, string | undefined> = {
ts: 'typescript',
js: 'javascript',
md: 'markdown',
c: 'c',
h: 'c',
txt: 'text'
}
const languageMapping = {
'ts': 'typescript',
'js': 'javascript',
'md': 'markdown',
'c': 'c',
'h': 'c',
'other': ''
} /* Initializes empty file to global state */
export const createNewFile = (name: string) => { export const createNewFile = (name: string) => {
const tempName = name.split('.'); const ext = getFileExtention(name) || ''
const fileExt = tempName[tempName.length - 1] || 'other';
const emptyFile: IFile = { name, language: languageMapping[fileExt as 'ts' | 'js' | 'md' | 'c' | 'h' | 'other'], content: "" }; const emptyFile: IFile = { name, language: languageMapping[ext] || 'text', content: '' }
state.files.push(emptyFile); state.files.push(emptyFile)
state.active = state.files.length - 1; state.active = state.files.length - 1
}; }
export const renameFile = (oldName: string, nwName: string) => {
const file = state.files.find(file => file.name === oldName)
if (!file) throw Error(`No file exists with name ${oldName}`)
const ext = getFileExtention(nwName) || ''
const language = languageMapping[ext] || 'text'
file.name = nwName
file.language = language
}

View File

@@ -0,0 +1,17 @@
import state, { transactionsState } from '..'
export const deleteAccount = (addr?: string) => {
if (!addr) return
const index = state.accounts.findIndex(acc => acc.address === addr)
if (index === -1) return
state.accounts.splice(index, 1)
// update selected accounts
transactionsState.transactions
.filter(t => t.state.selectedAccount?.value === addr)
.forEach(t => {
const acc = t.state.selectedAccount
if (!acc) return
acc.label = acc.value
})
}

View File

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

View File

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

View File

@@ -1,92 +1,109 @@
import { Octokit } from "@octokit/core"; import { Octokit } from '@octokit/core'
import Router from "next/router"; import state, { IFile } from '../index'
import state from '../index'; import { templateFileIds } from '../constants'
import { templateFileIds } from '../constants';
const octokit = new Octokit(); const octokit = new Octokit()
/* Fetches Gist files from Githug Gists based on /**
* gistId and stores the content in global state * Fetches files from Github Gists based on gistId and stores them in global state
*/ */
export const fetchFiles = (gistId: string) => { export const fetchFiles = async (gistId: string) => {
state.loading = true; if (!gistId || state.files.length) return
if (gistId && !state.files.length) {
state.loading = true
state.logs.push({
type: 'log',
message: `Fetching Gist with id: ${gistId}`
})
try {
const res = await octokit.request('GET /gists/{gist_id}', { gist_id: gistId })
const isTemplate = (id: string) =>
Object.values(templateFileIds)
.map(v => v.id)
.includes(id)
if (isTemplate(gistId)) {
const template = Object.values(templateFileIds).find(tmp => tmp.id === gistId)
let headerFiles: Record<string, { filename: string; content: string; language: string }> =
{}
if (template?.headerId) {
const resHeader = await octokit.request('GET /gists/{gist_id}', { gist_id: template.headerId })
if (!resHeader.data.files) throw new Error('No header files could be fetched from given gist id!')
headerFiles = resHeader.data.files as any
} else {
// fetch headers
const headerRes = await fetch(
`${process.env.NEXT_PUBLIC_COMPILE_API_BASE_URL}/api/header-files`
)
if (!headerRes.ok) throw Error('Failed to fetch headers')
const headerJson = await headerRes.json()
const headerFiles: Record<string, { filename: string; content: string; language: string }> =
{}
Object.entries(headerJson).forEach(([key, value]) => {
const fname = `${key}.h`
headerFiles[fname] = { filename: fname, content: value as string, language: 'C' }
})
}
const files = {
...res.data.files,
...headerFiles
}
res.data.files = files
}
if (!res.data.files) throw Error('No files could be fetched from given gist id!')
const files: IFile[] = Object.keys(res.data.files).map(filename => ({
name: res.data.files?.[filename]?.filename || 'untitled.c',
language: res.data.files?.[filename]?.language?.toLowerCase() || '',
content: res.data.files?.[filename]?.content || ''
}))
files.sort((a, b) => {
const aBasename = a.name.split('.')?.[0]
const aExt = a.name.split('.').pop() || ''
const bBasename = b.name.split('.')?.[0]
const bExt = b.name.split('.').pop() || ''
// default priority is undefined == 0
const extPriority: Record<string, number> = {
c: 3,
wat: 3,
md: 2,
h: -1
}
// Sort based on extention priorities
const comp = (extPriority[bExt] || 0) - (extPriority[aExt] || 0)
if (comp !== 0) return comp
// Otherwise fallback to alphabetical sorting
return aBasename.localeCompare(bBasename)
})
state.logs.push({ state.logs.push({
type: "log", type: 'success',
message: `Fetching Gist with id: ${gistId}`, message: 'Fetched successfully ✅'
}); })
state.files = files
state.gistId = gistId
state.gistOwner = res.data.owner?.login
octokit const gistName =
.request("GET /gists/{gist_id}", { gist_id: gistId }) files.find(file => file.language === 'c' || file.language === 'javascript')?.name ||
.then(async res => { 'untitled'
if (!Object.values(templateFileIds).includes(gistId)) { state.gistName = gistName
return res } catch (err) {
} console.error(err)
// in case of templates, fetch header file(s) and append to res let message: string
let resHeaderJson; if (err instanceof Error) message = err.message
try { else message = `Something went wrong, try again later!`
const resHeader = await fetch(`${process.env.NEXT_PUBLIC_COMPILE_API_BASE_URL}/api/header-files`); state.logs.push({
if (resHeader.ok) { type: 'error',
resHeaderJson = await resHeader.json(); message: `Error: ${message}`
const files = { })
...res.data.files,
'hookapi.h': res.data.files?.['hookapi.h'] || { filename: 'hookapi.h', content: resHeaderJson.hookapi, language: 'C' },
'hookmacro.h': res.data.files?.['hookmacro.h'] || { filename: 'hookmacro.h', content: resHeaderJson.hookmacro, language: 'C' },
'sfcodes.h': res.data.files?.['sfcodes.h'] || { filename: 'sfcodes.h', content: resHeaderJson.sfcodes, language: 'C' },
};
res.data.files = files;
}
} catch (err) {
console.log(err)
}
return res;
// If you want to load templates from GIST instad, uncomment the code below and comment the code above.
// return octokit.request("GET /gists/{gist_id}", { gist_id: templateFileIds.headers }).then(({ data: { files: headerFiles } }) => {
// const files = { ...res.data.files, ...headerFiles }
// console.log(headerFiles)
// res.data.files = files
// return res
// })
})
.then((res) => {
if (res.data.files && Object.keys(res.data.files).length > 0) {
const files = Object.keys(res.data.files).map((filename) => ({
name: res.data.files?.[filename]?.filename || "untitled.c",
language: res.data.files?.[filename]?.language?.toLowerCase() || "",
content: res.data.files?.[filename]?.content || "",
}));
state.loading = false;
if (files.length > 0) {
state.logs.push({
type: "success",
message: "Fetched successfully ✅",
});
state.files = files;
state.gistId = gistId;
state.gistName = Object.keys(res.data.files)?.[0] || "untitled";
state.gistOwner = res.data.owner?.login;
return;
} else {
// Open main modal if now files
state.mainModalOpen = true;
}
return Router.push({ pathname: "/develop" });
}
state.loading = false;
})
.catch((err) => {
// console.error(err)
state.loading = false;
state.logs.push({
type: "error",
message: `Couldn't find Gist with id: ${gistId}`,
});
return;
});
return;
} }
state.loading = false; state.loading = false
}; }

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { snapshot } from "valtio" import { snapshot } from 'valtio'
import state from ".." import state from '..'
export type SplitSize = number[] export type SplitSize = number[]
@@ -12,4 +12,3 @@ export const getSplit = (splitId: string): SplitSize | null => {
const split = splits[splitId] const split = splits[splitId]
return split ? split : null return split ? split : null
} }

View File

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

View File

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

View File

@@ -0,0 +1,80 @@
import { derive, sign } from 'xrpl-accountlib'
import state from '..'
import type { IAccount } from '..'
import ResultLink from '../../components/ResultLink'
import { ref } from 'valtio'
import { xrplSend } from './xrpl-client'
interface TransactionOptions {
TransactionType: string
Account?: string
Fee?: string
[index: string]: any
}
interface OtherOptions {
logPrefix?: string
}
export const sendTransaction = async (
account: IAccount,
txOptions: TransactionOptions,
options?: OtherOptions
) => {
const { Fee = '1000', ...opts } = txOptions
const tx: TransactionOptions = {
Account: account.address,
Sequence: account.sequence,
Fee,
NetworkID: process.env.NEXT_PUBLIC_NETWORK_ID,
...opts
}
const { logPrefix = '' } = options || {}
state.transactionLogs.push({
type: 'log',
message: `${logPrefix}${JSON.stringify(tx, null, 2)}`
})
try {
const signedAccount = derive.familySeed(account.secret)
const { signedTransaction } = sign(tx, signedAccount)
const response = await xrplSend({
command: 'submit',
tx_blob: signedTransaction
})
const resultMsg = ref(
<>
{logPrefix}[<ResultLink result={response.engine_result} />] {response.engine_result_message}
</>
)
if (response.engine_result === 'tesSUCCESS') {
state.transactionLogs.push({
type: 'success',
message: resultMsg
})
} else if (response.engine_result) {
state.transactionLogs.push({
type: 'error',
message: resultMsg
})
} else {
state.transactionLogs.push({
type: 'error',
message: `${logPrefix}[${response.error}] ${response.error_exception}`
})
}
const currAcc = state.accounts.find(acc => acc.address === account.address)
if (currAcc && response.account_sequence_next) {
currAcc.sequence = response.account_sequence_next
}
} catch (err) {
console.error(err)
state.transactionLogs.push({
type: 'error',
message:
err instanceof Error
? `${logPrefix}Error: ${err.message}`
: `${logPrefix}Something went wrong, try again later`
})
}
}

View File

@@ -1,23 +1,27 @@
import { ref } from 'valtio'; import { ref } from 'valtio'
import { AlertState, alertState } from "../../components/AlertDialog"; import { AlertState, alertState } from '../../components/AlertDialog'
export const showAlert = (title: string, opts: Omit<Partial<AlertState>, 'title' | 'isOpen'> = {}) => { export const showAlert = (
const { body: _body, confirmPrefix: _confirmPrefix, ...rest } = opts title: string,
const body = (_body && typeof _body === 'object') ? ref(_body) : _body opts: Omit<Partial<AlertState>, 'title' | 'isOpen'> = {}
const confirmPrefix = (_confirmPrefix && typeof _confirmPrefix === 'object') ? ref(_confirmPrefix) : _confirmPrefix ) => {
const { body: _body, confirmPrefix: _confirmPrefix, ...rest } = opts
const body = _body && typeof _body === 'object' ? ref(_body) : _body
const confirmPrefix =
_confirmPrefix && typeof _confirmPrefix === 'object' ? ref(_confirmPrefix) : _confirmPrefix
const nwState: AlertState = { const nwState: AlertState = {
isOpen: true, isOpen: true,
title, title,
body, body,
confirmPrefix, confirmPrefix,
cancelText: undefined, cancelText: undefined,
confirmText: undefined, confirmText: undefined,
onCancel: undefined, onCancel: undefined,
onConfirm: undefined, onConfirm: undefined,
...rest, ...rest
} }
Object.entries(nwState).forEach(([key, value]) => { Object.entries(nwState).forEach(([key, value]) => {
(alertState as any)[key] = value ;(alertState as any)[key] = value
}) })
} }

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
import { XrplClient } from 'xrpl-client';
import state from '..';
export const xrplSend = async(...params: Parameters<XrplClient['send']>) => {
const client = await state.client.ready()
return client.send(...params);
}

82
state/constants/flags.ts Normal file
View File

@@ -0,0 +1,82 @@
import { SelectOption } from '../transactions';
interface Flags {
[key: string]: string;
}
export const transactionFlags: { [key: /* TransactionType */ string]: Flags } = {
"*": {
tfFullyCanonicalSig: '0x80000000'
},
Payment: {
tfNoDirectRipple: '0x00010000',
tfPartialPayment: '0x00020000',
tfLimitQuality: '0x00040000',
},
AccountSet: {
tfRequireDestTag: '0x00010000',
tfOptionalDestTag: '0x00020000',
tfRequireAuth: '0x00040000',
tfOptionalAuth: '0x00080000',
tfDisallowXRP: '0x00100000',
tfAllowXRP: '0x00200000',
},
NFTokenCreateOffer: {
tfSellNFToken: '0x00000001',
},
NFTokenMint: {
tfBurnable: '0x00000001',
tfOnlyXRP: '0x00000002',
tfTrustLine: '0x00000004',
tfTransferable: '0x00000008',
},
OfferCreate: {
tfPassive: '0x00010000',
tfImmediateOrCancel: '0x00020000',
tfFillOrKill: '0x00040000',
tfSell: '0x00080000',
},
PaymentChannelClaim: {
tfRenew: '0x00010000',
tfClose: '0x00020000',
},
TrustSet: {
tfSetfAuth: '0x00010000',
tfSetNoRipple: '0x00020000',
tfClearNoRipple: '0x00040000',
tfSetFreeze: '0x00100000',
tfClearFreeze: '0x00200000',
},
URITokenMint: {
tfBurnable: '0x00000001',
},
}
export const getFlags = (tt?: string) => {
if (!tt) return
const flags = {
...transactionFlags['*'],
...transactionFlags[tt]
}
return flags
}
export function combineFlags(flags?: string[]): string | undefined {
if (!flags) return
const num = flags.reduce((cumm, curr) => cumm | BigInt(curr), BigInt(0))
return num.toString()
}
export function extractFlags(transactionType: string, flags?: string | number,): SelectOption[] {
const flagsObj = getFlags(transactionType)
if (!flags || !flagsObj) return []
const extracted = Object.entries(flagsObj).reduce((cumm, [label, value]) => {
return (BigInt(flags) & BigInt(value)) ? cumm.concat({ label, value }) : cumm
}, [] as SelectOption[])
return extracted
}

View File

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

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