Compare commits
	
		
			359 Commits
		
	
	
		
			fix/do-not
			...
			fix/tab-sw
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					1f5a9731bb | ||
| 
						 | 
					ef4f95ca3e | ||
| 
						 | 
					fb9814ec76 | ||
| 
						 | 
					d459b2ee92 | ||
| 
						 | 
					6ee1a09aaa | ||
| 
						 | 
					dd2228fb35 | ||
| 
						 | 
					ca52a5e064 | ||
| 
						 | 
					df0f8abe62 | ||
| 
						 | 
					a6c4db1951 | ||
| 
						 | 
					1c91003164 | ||
| 
						 | 
					66be0efbbd | ||
| 
						 | 
					9ab64ec062 | ||
| 
						 | 
					e77a5e234f | ||
| 
						 | 
					d2f618512a | ||
| 
						 | 
					f5063de2c9 | ||
| 
						 | 
					1ee8dcb536 | ||
| 
						 | 
					7f6f9c11db | ||
| 
						 | 
					b2b7059774 | ||
| 
						 | 
					41ba096ef9 | ||
| 
						 | 
					8b72086c04 | ||
| 
						 | 
					895b34cc68 | ||
| 
						 | 
					b9da659f83 | ||
| 
						 | 
					3897f2d823 | ||
| 
						 | 
					6a3ff3e1d7 | ||
| 
						 | 
					bf792f1495 | ||
| 
						 | 
					df3210a663 | ||
| 
						 | 
					bad7730c32 | ||
| 
						 | 
					adb6a78549 | ||
| 
						 | 
					8cc27f20c3 | ||
| 
						 | 
					8e49a3f5f1 | ||
| 
						 | 
					3179757469 | ||
| 
						 | 
					554cfb3db9 | ||
| 
						 | 
					637a066f69 | ||
| 
						 | 
					c9a852e9be | ||
| 
						 | 
					307a5407eb | ||
| 
						 | 
					faa28845c8 | ||
| 
						 | 
					168d11d48e | ||
| 
						 | 
					60f2bb558c | ||
| 
						 | 
					fdf33b9f45 | ||
| 
						 | 
					d05180d148 | ||
| 
						 | 
					bfaa6be17d | ||
| 
						 | 
					9e368dec84 | ||
| 
						 | 
					25eec6980f | ||
| 
						 | 
					8e2f20c5ac | ||
| 
						 | 
					a3d094e873 | ||
| 
						 | 
					ef70bfb13a | ||
| 
						 | 
					c26c7c13d1 | ||
| 
						 | 
					fc461ddd0d | ||
| 
						 | 
					c7001f6089 | ||
| 
						 | 
					243cbfec08 | ||
| 
						 | 
					1295e7fa41 | ||
| 
						 | 
					793623d216 | ||
| 
						 | 
					0cde0eb240 | ||
| 
						 | 
					e2acb48e03 | ||
| 
						 | 
					a4373bb970 | ||
| 
						 | 
					cfb791092a | ||
| 
						 | 
					3fcbac5ed9 | ||
| 
						 | 
					c40b272ce8 | ||
| 
						 | 
					860ff66a8a | ||
| 
						 | 
					f4f700bea1 | ||
| 
						 | 
					789bc00ac3 | ||
| 
						 | 
					6a0aabdeda | ||
| 
						 | 
					175b6266e8 | ||
| 
						 | 
					621482e2ee | ||
| 
						 | 
					e55f48bc83 | ||
| 
						 | 
					3e9e26a46a | ||
| 
						 | 
					f0e730bb9b | ||
| 
						 | 
					6ce4828fc6 | ||
| 
						 | 
					bb0a246ae5 | ||
| 
						 | 
					3af2bad536 | ||
| 
						 | 
					4f1b877db0 | ||
| 
						 | 
					0289d64f5e | ||
| 
						 | 
					868a0bcf78 | ||
| 
						 | 
					aab2476a05 | ||
| 
						 | 
					cb25986d72 | ||
| 
						 | 
					309ad57173 | ||
| 
						 | 
					53afb1d3d1 | ||
| 
						 | 
					31ff7c0e28 | ||
| 
						 | 
					dfa35df465 | ||
| 
						 | 
					f163b052e1 | ||
| 
						 | 
					25c5b9c015 | ||
| 
						 | 
					407e3946ce | ||
| 
						 | 
					dc5b0d71eb | ||
| 
						 | 
					3fd6c3f50e | ||
| 
						 | 
					ec8bfc5eee | ||
| 
						 | 
					b4a0bcb90d | ||
| 
						 | 
					2c729e2aa4 | ||
| 
						 | 
					1cb2542170 | ||
| 
						 | 
					00b309df34 | ||
| 
						 | 
					a6fc730de6 | ||
| 
						 | 
					2245c5a221 | ||
| 
						 | 
					60c33661ad | ||
| 
						 | 
					ea21c85038 | ||
| 
						 | 
					5478f43609 | ||
| 
						 | 
					a9b64abb85 | ||
| 
						 | 
					c6ced424d8 | ||
| 
						 | 
					3a1159cffc | ||
| 
						 | 
					3136de1bd1 | ||
| 
						 | 
					67ffd3f1b4 | ||
| 
						 | 
					8508cb69c4 | ||
| 
						 | 
					89217d2633 | ||
| 
						 | 
					ba1b64391c | ||
| 
						 | 
					098d919a77 | ||
| 
						 | 
					b2af37ab4b | ||
| 
						 | 
					dcb7e94e86 | ||
| 
						 | 
					67848b3d8d | ||
| 
						 | 
					31a86263a1 | ||
| 
						 | 
					4d0025afc1 | ||
| 
						 | 
					f85bd2398d | ||
| 
						 | 
					a2a6596cc5 | ||
| 
						 | 
					37208ce97e | ||
| 
						 | 
					bf4042926d | ||
| 
						 | 
					3ccc1c16ac | ||
| 
						 | 
					135f0c91a1 | ||
| 
						 | 
					8f5786e242 | ||
| 
						 | 
					810eb4ca27 | ||
| 
						 | 
					e6574f9f12 | ||
| 
						 | 
					1a6726fabf | ||
| 
						 | 
					89f8671217 | ||
| 
						 | 
					fb5259221b | ||
| 
						 | 
					fd17f59616 | ||
| 
						 | 
					91bbc7ea61 | ||
| 
						 | 
					783d832c6d | ||
| 
						 | 
					698ca376e7 | ||
| 
						 | 
					bfd9e21ab8 | ||
| 
						 | 
					e46411f245 | ||
| 
						 | 
					08447c6b29 | ||
| 
						 | 
					9216cc6bf7 | ||
| 
						 | 
					5108b08e39 | ||
| 
						 | 
					6c46a4f809 | ||
| 
						 | 
					0ea88f0d32 | ||
| 
						 | 
					4c2e1f36f3 | ||
| 
						 | 
					fa5315fc0e | ||
| 
						 | 
					eda8b1550c | ||
| 
						 | 
					742b11374f | ||
| 
						 | 
					d16e83dcfa | ||
| 
						 | 
					155aa57784 | ||
| 
						 | 
					b88b6da7d9 | ||
| 
						 | 
					fa13f7e282 | ||
| 
						 | 
					f1a43ef758 | ||
| 
						 | 
					4217813fd7 | ||
| 
						 | 
					c588f7b1f3 | ||
| 
						 | 
					985e8ee820 | ||
| 
						 | 
					8832e76a0a | ||
| 
						 | 
					9777f1dbd1 | ||
| 
						 | 
					213d468aab | ||
| 
						 | 
					46becb0e7b | ||
| 
						 | 
					fad6bd100a | ||
| 
						 | 
					5a11f83fea | ||
| 
						 | 
					525338abf7 | ||
| 
						 | 
					ea977816a4 | ||
| 
						 | 
					0ee599a2b6 | ||
| 
						 | 
					02c59f8d79 | ||
| 
						 | 
					3d5b77e60a | ||
| 
						 | 
					92a167d47a | ||
| 
						 | 
					d41e263942 | ||
| 
						 | 
					bd1226fe90 | ||
| 
						 | 
					57403e42dd | ||
| 
						 | 
					2b42a96c4a | ||
| 
						 | 
					80d6bb691d | ||
| 
						 | 
					c7e4cd7c92 | ||
| 
						 | 
					4a22861860 | ||
| 
						 | 
					b09d029931 | ||
| 
						 | 
					b2dc49754f | ||
| 
						 | 
					6f636645f7 | ||
| 
						 | 
					377c963c7a | ||
| 
						 | 
					ae038f17ff | ||
| 
						 | 
					0d8f2c31e7 | ||
| 
						 | 
					da9986eb66 | ||
| 
						 | 
					a21350770e | ||
| 
						 | 
					49dfd43220 | ||
| 
						 | 
					4472957f5c | ||
| 
						 | 
					ca46707bb5 | ||
| 
						 | 
					704ebe4b92 | ||
| 
						 | 
					9a6ef2c393 | ||
| 
						 | 
					56203ce9c6 | ||
| 
						 | 
					933bdb5968 | ||
| 
						 | 
					864711697b | ||
| 
						 | 
					e5eaf09721 | ||
| 
						 | 
					d0dde56c67 | ||
| 
						 | 
					45c6927e72 | ||
| 
						 | 
					6014b6e79f | ||
| 
						 | 
					04a99227df | ||
| 
						 | 
					0965a1e898 | ||
| 
						 | 
					32445dbebf | ||
| 
						 | 
					1a1d4901aa | ||
| 
						 | 
					8b646c56dc | ||
| 
						 | 
					ac38bbc72c | ||
| 
						 | 
					bf1182351a | ||
| 
						 | 
					55e48a943b | ||
| 
						 | 
					faf417be69 | ||
| 
						 | 
					c2eb57211f | ||
| 
						 | 
					0e97df3c8e | ||
| 
						 | 
					5dd0dfdc18 | ||
| 
						 | 
					ef48bac8f6 | ||
| 
						 | 
					3a3d984098 | ||
| 
						 | 
					2300c201f8 | ||
| 
						 | 
					329dc4a355 | ||
| 
						 | 
					cd6a5b23d4 | ||
| 
						 | 
					4dd7cbe2ca | ||
| 
						 | 
					260de7c838 | ||
| 
						 | 
					e0ed31f220 | ||
| 
						 | 
					eba183497f | ||
| 
						 | 
					4378afa9a1 | ||
| 
						 | 
					491e10920b | ||
| 
						 | 
					65bb209713 | ||
| 
						 | 
					c07e70acc9 | ||
| 
						 | 
					8fd7f8ecad | ||
| 
						 | 
					2bb3c646db | ||
| 
						 | 
					87f10a11b0 | ||
| 
						 | 
					949fb45ae2 | ||
| 
						 | 
					ea52f014dd | ||
| 
						 | 
					77eab8d88d | ||
| 
						 | 
					4ca8f5f236 | ||
| 
						 | 
					53f2a71b08 | ||
| 
						 | 
					866f6257f1 | ||
| 
						 | 
					814b074cc0 | ||
| 
						 | 
					386619619b | ||
| 
						 | 
					d8bf10d0b8 | ||
| 
						 | 
					d18c893025 | ||
| 
						 | 
					822f7a30f5 | ||
| 
						 | 
					1d66137c23 | ||
| 
						 | 
					4c42e75686 | ||
| 
						 | 
					501b7fefec | ||
| 
						 | 
					aa7e1517a2 | ||
| 
						 | 
					e33093f160 | ||
| 
						 | 
					923b689c98 | ||
| 
						 | 
					246e7f137f | ||
| 
						 | 
					5defd12a11 | ||
| 
						 | 
					abb7c2bb28 | ||
| 
						 | 
					12013907f8 | ||
| 
						 | 
					ec75fff74b | ||
| 
						 | 
					5e997044ed | ||
| 
						 | 
					e88720327e | ||
| 
						 | 
					bf568c3f46 | ||
| 
						 | 
					7c1068449f | ||
| 
						 | 
					b66d2a09a0 | ||
| 
						 | 
					1d3bd128f8 | ||
| 
						 | 
					ab1f45febd | ||
| 
						 | 
					54265e024c | ||
| 
						 | 
					20cb66ba18 | ||
| 
						 | 
					56a9806b70 | ||
| 
						 | 
					b3f2d0fb6d | ||
| 
						 | 
					b7d62dda83 | ||
| 
						 | 
					c690334f92 | ||
| 
						 | 
					587f09ec00 | ||
| 
						 | 
					9296ea1acc | ||
| 
						 | 
					582fb17c94 | ||
| 
						 | 
					aff0142870 | ||
| 
						 | 
					df51d87cb2 | ||
| 
						 | 
					6a46f5f173 | ||
| 
						 | 
					9e25cefef9 | ||
| 
						 | 
					95966fa514 | ||
| 
						 | 
					f49d69e75d | ||
| 
						 | 
					da4b2e68ab | ||
| 
						 | 
					5557b1bcba | ||
| 
						 | 
					f4b5f98a44 | ||
| 
						 | 
					b1d39740de | ||
| 
						 | 
					dfe5589074 | ||
| 
						 | 
					cdc50da840 | ||
| 
						 | 
					4893b41936 | ||
| 
						 | 
					16cbdafb27 | ||
| 
						 | 
					5559fb7be3 | ||
| 
						 | 
					3c4305127b | ||
| 
						 | 
					2a76fa0c35 | ||
| 
						 | 
					bf21fe36c3 | ||
| 
						 | 
					a33a3eb6e2 | ||
| 
						 | 
					919c4e173c | ||
| 
						 | 
					650324f434 | ||
| 
						 | 
					74db96e8a5 | ||
| 
						 | 
					c99c821081 | ||
| 
						 | 
					e53a533026 | ||
| 
						 | 
					5f118e00cb | ||
| 
						 | 
					46e6927c68 | ||
| 
						 | 
					de95a82c5a | ||
| 
						 | 
					3b66d64c14 | ||
| 
						 | 
					6e90a4c3d7 | ||
| 
						 | 
					2287e7babb | ||
| 
						 | 
					c219f7ea00 | ||
| 
						 | 
					e795ce4472 | ||
| 
						 | 
					04e2274dbf | ||
| 
						 | 
					6e39b90c1e | ||
| 
						 | 
					f186a807c1 | ||
| 
						 | 
					5ad9ed1688 | ||
| 
						 | 
					842b8a5226 | ||
| 
						 | 
					234832138f | ||
| 
						 | 
					28d94a1475 | ||
| 
						 | 
					594aee6cd2 | ||
| 
						 | 
					d75910972f | ||
| 
						 | 
					589c604a12 | ||
| 
						 | 
					8394a11705 | ||
| 
						 | 
					4ad329882c | ||
| 
						 | 
					ee86b91e82 | ||
| 
						 | 
					d2addf782e | ||
| 
						 | 
					51f7bd509b | ||
| 
						 | 
					e064251ff9 | ||
| 
						 | 
					5aeed7c246 | ||
| 
						 | 
					8d03edc299 | ||
| 
						 | 
					95022ef121 | ||
| 
						 | 
					4519906b78 | ||
| 
						 | 
					88a47c49a4 | ||
| 
						 | 
					1ab03f9bed | ||
| 
						 | 
					84ff667135 | ||
| 
						 | 
					0d10e782f3 | ||
| 
						 | 
					84e6763495 | ||
| 
						 | 
					7ffcfd694d | ||
| 
						 | 
					77e4917d38 | ||
| 
						 | 
					e4238a40cc | ||
| 
						 | 
					42c0b20512 | ||
| 
						 | 
					43154ff6d8 | ||
| 
						 | 
					8197b510f9 | ||
| 
						 | 
					fc7652f48e | ||
| 
						 | 
					bd32555617 | ||
| 
						 | 
					fc6f420e1e | ||
| 
						 | 
					d3c36765de | ||
| 
						 | 
					2628a12673 | ||
| 
						 | 
					f6c1869b5d | ||
| 
						 | 
					62c8b4f217 | ||
| 
						 | 
					8798e5a233 | ||
| 
						 | 
					5f7d42843c | ||
| 
						 | 
					302b36dde8 | ||
| 
						 | 
					936bbc503a | ||
| 
						 | 
					81890c8833 | ||
| 
						 | 
					50fa20c39a | ||
| 
						 | 
					11f2cffc87 | ||
| 
						 | 
					bbd1d162f0 | ||
| 
						 | 
					b301a860bf | ||
| 
						 | 
					ff697b96ea | ||
| 
						 | 
					48e9898e31 | ||
| 
						 | 
					2e25242ebe | ||
| 
						 | 
					e32e07f7fd | ||
| 
						 | 
					0d2a17008e | ||
| 
						 | 
					a87b3de6c4 | ||
| 
						 | 
					23068ff477 | ||
| 
						 | 
					a12a5dfbac | ||
| 
						 | 
					5a598cb091 | ||
| 
						 | 
					be39054a2f | ||
| 
						 | 
					0add65dd1c | ||
| 
						 | 
					82170ad4f8 | ||
| 
						 | 
					af49426eb0 | ||
| 
						 | 
					48a86e3386 | ||
| 
						 | 
					c82c35b5a1 | ||
| 
						 | 
					f849be1f80 | ||
| 
						 | 
					694d07fa0e | ||
| 
						 | 
					b9aa3e2adc | ||
| 
						 | 
					5b573b2379 | ||
| 
						 | 
					23538b1502 | ||
| 
						 | 
					723602ebdc | ||
| 
						 | 
					f8fdeaf9ce | ||
| 
						 | 
					e75b971718 | ||
| 
						 | 
					11a35a5932 | ||
| 
						 | 
					611f875761 | ||
| 
						 | 
					a7df50c194 | ||
| 
						 | 
					e82662647f | ||
| 
						 | 
					d8e218392a | ||
| 
						 | 
					723722df58 | ||
| 
						 | 
					2ff85ede06 | ||
| 
						 | 
					7f8f47cb14 | ||
| 
						 | 
					09f58f18ae | 
@@ -1,5 +1,11 @@
 | 
			
		||||
NEXTAUTH_URL=https://example.com
 | 
			
		||||
NEXTAUTH_SECRET="1234"
 | 
			
		||||
GITHUB_SECRET=""
 | 
			
		||||
GITHUB_ID=""
 | 
			
		||||
NEXT_PUBLIC_COMPILE_API_ENDPOINT="http://localhost:9000/api/build"
 | 
			
		||||
NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT="ws://localhost:9000/language-server/c"
 | 
			
		||||
NEXT_PUBLIC_COMPILE_API_BASE_URL="http://localhost:9000"
 | 
			
		||||
NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT="ws://localhost:9000/language-server/c"
 | 
			
		||||
NEXT_PUBLIC_TESTNET_URL="hooks-testnet-v2.xrpl-labs.com"
 | 
			
		||||
NEXT_PUBLIC_DEBUG_STREAM_URL="hooks-testnet-v2-debugstream.xrpl-labs.com"
 | 
			
		||||
NEXT_PUBLIC_EXPLORER_URL="hooks-testnet-v2-explorer.xrpl-labs.com"
 | 
			
		||||
NEXT_PUBLIC_SITE_URL=http://localhost:3000
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -32,3 +32,4 @@ yarn-error.log*
 | 
			
		||||
 | 
			
		||||
# vercel
 | 
			
		||||
.vercel
 | 
			
		||||
.vscode
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								README.md
									
									
									
									
									
								
							@@ -1,6 +1,8 @@
 | 
			
		||||
# XRPL Hooks IDE
 | 
			
		||||
# XRPL Hooks Builder
 | 
			
		||||
 | 
			
		||||
This is the repository for XRPL Hooks IDE. This project is built with Next.JS
 | 
			
		||||
https://hooks-builder.xrpl.org/
 | 
			
		||||
 | 
			
		||||
This is the repository for XRPL Hooks Builder. This project is built with Next.JS
 | 
			
		||||
 | 
			
		||||
## General
 | 
			
		||||
 | 
			
		||||
@@ -87,6 +89,9 @@ By default `@monaco-editor/react` was using 0.29.? version of Monaco editor. @co
 | 
			
		||||
 | 
			
		||||
Monaco Languageclient related stuff is found from `./utils/languageClient.ts`. Basically we're connecting the editor to clangd language server which lives on separate backend. That project can be found from https://github.com/eqlabs/xrpl-hooks-compiler/. If you need access to that project ask permissions from @vbar (Vaclav Barta) on GitHub.
 | 
			
		||||
 | 
			
		||||
### Language server hover messages
 | 
			
		||||
If you want to extend hover messages provided by language-server you can add extra docs to `xrpl-hooks-docs/md/` folder. Just make sure the filename is matching with the error code that comes from language server. So lets say you want to add extra documentation for `hooks-func-addr-taken` check create new file called `hooks-func-addr-taken.md` and then remember to import and export it on `docs.ts` file with same logic as the other files.
 | 
			
		||||
 | 
			
		||||
## Global state management
 | 
			
		||||
 | 
			
		||||
Global state management is handled with library called Valtio (https://github.com/pmndrs/valtio). Initial state can be found from `./state/index.ts` file. All the actions which updates the state is found under `./state/actions/` folder.
 | 
			
		||||
@@ -103,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.
 | 
			
		||||
 | 
			
		||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
import toast from "react-hot-toast";
 | 
			
		||||
import { useSnapshot } from "valtio";
 | 
			
		||||
import { ArrowSquareOut, Copy, Wallet, X } from "phosphor-react";
 | 
			
		||||
import { ArrowSquareOut, Copy, Trash, Wallet, X } from "phosphor-react";
 | 
			
		||||
import React, { useEffect, useState, FC } from "react";
 | 
			
		||||
import Dinero from "dinero.js";
 | 
			
		||||
 | 
			
		||||
import Button from "./Button";
 | 
			
		||||
import { addFaucetAccount, deployHook, importAccount } from "../state/actions";
 | 
			
		||||
import { addFaucetAccount, importAccount } from "../state/actions";
 | 
			
		||||
import state from "../state";
 | 
			
		||||
import Box from "./Box";
 | 
			
		||||
import { Container, Heading, Stack, Text, Flex } from ".";
 | 
			
		||||
@@ -18,7 +18,8 @@ import {
 | 
			
		||||
  DialogTrigger,
 | 
			
		||||
} from "./Dialog";
 | 
			
		||||
import { css } from "../stitches.config";
 | 
			
		||||
import { Input } from "./Input";
 | 
			
		||||
import { Input, Label } from "./Input";
 | 
			
		||||
import truncate from "../utils/truncate";
 | 
			
		||||
 | 
			
		||||
const labelStyle = css({
 | 
			
		||||
  color: "$mauve10",
 | 
			
		||||
@@ -26,6 +27,11 @@ const labelStyle = css({
 | 
			
		||||
  fontSize: "10px",
 | 
			
		||||
  mb: "$0.5",
 | 
			
		||||
});
 | 
			
		||||
import transactionsData from "../content/transactions.json";
 | 
			
		||||
import { SetHookDialog } from "./SetHookDialog";
 | 
			
		||||
import { addFunds } from "../state/actions/addFaucetAccount";
 | 
			
		||||
import { deleteHook } from "../state/actions/deployHook";
 | 
			
		||||
import { capitalize } from "../utils/helpers";
 | 
			
		||||
 | 
			
		||||
export const AccountDialog = ({
 | 
			
		||||
  activeAccountAddress,
 | 
			
		||||
@@ -37,12 +43,12 @@ export const AccountDialog = ({
 | 
			
		||||
  const snap = useSnapshot(state);
 | 
			
		||||
  const [showSecret, setShowSecret] = useState(false);
 | 
			
		||||
  const activeAccount = snap.accounts.find(
 | 
			
		||||
    (account) => account.address === activeAccountAddress
 | 
			
		||||
    account => account.address === activeAccountAddress
 | 
			
		||||
  );
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog
 | 
			
		||||
      open={Boolean(activeAccountAddress)}
 | 
			
		||||
      onOpenChange={(open) => {
 | 
			
		||||
      onOpenChange={open => {
 | 
			
		||||
        setShowSecret(false);
 | 
			
		||||
        !open && setActiveAccountAddress(null);
 | 
			
		||||
      }}
 | 
			
		||||
@@ -86,6 +92,22 @@ export const AccountDialog = ({
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Wallet size="15px" /> {activeAccount?.name}
 | 
			
		||||
          <DialogClose asChild>
 | 
			
		||||
            <Button
 | 
			
		||||
              size="xs"
 | 
			
		||||
              outline
 | 
			
		||||
              css={{ ml: "auto", mr: "$9" }}
 | 
			
		||||
              tabIndex={-1}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                const index = state.accounts.findIndex(
 | 
			
		||||
                  acc => acc.address === activeAccount?.address
 | 
			
		||||
                );
 | 
			
		||||
                state.accounts.splice(index, 1);
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              Delete Account <Trash size="15px" />
 | 
			
		||||
            </Button>
 | 
			
		||||
          </DialogClose>
 | 
			
		||||
        </DialogTitle>
 | 
			
		||||
        <DialogDescription as="div" css={{ fontFamily: "$monospace" }}>
 | 
			
		||||
          <Stack css={{ display: "flex", flexDirection: "column", gap: "$3" }}>
 | 
			
		||||
@@ -95,9 +117,16 @@ export const AccountDialog = ({
 | 
			
		||||
                <Text
 | 
			
		||||
                  css={{
 | 
			
		||||
                    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>
 | 
			
		||||
              </Flex>
 | 
			
		||||
              <Flex css={{ marginLeft: "auto", color: "$mauve12" }}>
 | 
			
		||||
@@ -137,7 +166,7 @@ export const AccountDialog = ({
 | 
			
		||||
                    }}
 | 
			
		||||
                    ghost
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    onClick={() => setShowSecret((curr) => !curr)}
 | 
			
		||||
                    onClick={() => setShowSecret(curr => !curr)}
 | 
			
		||||
                  >
 | 
			
		||||
                    {showSecret ? "Hide" : "Show"}
 | 
			
		||||
                  </Button>
 | 
			
		||||
@@ -163,6 +192,8 @@ export const AccountDialog = ({
 | 
			
		||||
                <Text
 | 
			
		||||
                  css={{
 | 
			
		||||
                    fontFamily: "$monospace",
 | 
			
		||||
                    display: "flex",
 | 
			
		||||
                    alignItems: "center",
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  {Dinero({
 | 
			
		||||
@@ -175,11 +206,30 @@ export const AccountDialog = ({
 | 
			
		||||
                      currency: "XRP",
 | 
			
		||||
                      currencyDisplay: "name",
 | 
			
		||||
                    })}
 | 
			
		||||
                  <Button
 | 
			
		||||
                    css={{
 | 
			
		||||
                      fontFamily: "$monospace",
 | 
			
		||||
                      lineHeight: 2,
 | 
			
		||||
                      mt: "2px",
 | 
			
		||||
                      ml: "$3",
 | 
			
		||||
                    }}
 | 
			
		||||
                    ghost
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      addFunds(activeAccount?.address || "");
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    Add Funds
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Text>
 | 
			
		||||
              </Flex>
 | 
			
		||||
              <Flex css={{ marginLeft: "auto" }}>
 | 
			
		||||
              <Flex
 | 
			
		||||
                css={{
 | 
			
		||||
                  marginLeft: "auto",
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <a
 | 
			
		||||
                  href={`https://hooks-testnet-explorer.xrpl-labs.com/${activeAccount?.address}`}
 | 
			
		||||
                  href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
 | 
			
		||||
                  target="_blank"
 | 
			
		||||
                  rel="noreferrer noopener"
 | 
			
		||||
                >
 | 
			
		||||
@@ -199,11 +249,40 @@ export const AccountDialog = ({
 | 
			
		||||
                <Text
 | 
			
		||||
                  css={{
 | 
			
		||||
                    fontFamily: "$monospace",
 | 
			
		||||
                    a: { "&:hover": { textDecoration: "underline" } },
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  {activeAccount && activeAccount.hooks.length}
 | 
			
		||||
                  {activeAccount && activeAccount.hooks.length > 0
 | 
			
		||||
                    ? activeAccount.hooks.map(i => {
 | 
			
		||||
                        return (
 | 
			
		||||
                          <a
 | 
			
		||||
                            key={i}
 | 
			
		||||
                            href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${i}`}
 | 
			
		||||
                            target="_blank"
 | 
			
		||||
                            rel="noopener noreferrer"
 | 
			
		||||
                          >
 | 
			
		||||
                            {truncate(i, 12)}
 | 
			
		||||
                          </a>
 | 
			
		||||
                        );
 | 
			
		||||
                      })
 | 
			
		||||
                    : "–"}
 | 
			
		||||
                </Text>
 | 
			
		||||
              </Flex>
 | 
			
		||||
              {activeAccount && activeAccount?.hooks?.length > 0 && (
 | 
			
		||||
                <Flex css={{ marginLeft: "auto" }}>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    outline
 | 
			
		||||
                    disabled={activeAccount.isLoading}
 | 
			
		||||
                    css={{ mt: "$3", mr: "$1", ml: "auto" }}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      deleteHook(activeAccount);
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    Delete Hook <Trash size="15px" />
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Flex>
 | 
			
		||||
              )}
 | 
			
		||||
            </Flex>
 | 
			
		||||
          </Stack>
 | 
			
		||||
        </DialogDescription>
 | 
			
		||||
@@ -223,7 +302,7 @@ interface AccountProps {
 | 
			
		||||
  showHookStats?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Accounts: FC<AccountProps> = (props) => {
 | 
			
		||||
const Accounts: FC<AccountProps> = props => {
 | 
			
		||||
  const snap = useSnapshot(state);
 | 
			
		||||
  const [activeAccountAddress, setActiveAccountAddress] = useState<
 | 
			
		||||
    string | null
 | 
			
		||||
@@ -231,9 +310,9 @@ const Accounts: FC<AccountProps> = (props) => {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const fetchAccInfo = async () => {
 | 
			
		||||
      if (snap.clientStatus === "online") {
 | 
			
		||||
        const requests = snap.accounts.map((acc) =>
 | 
			
		||||
        const requests = snap.accounts.map(acc =>
 | 
			
		||||
          snap.client?.send({
 | 
			
		||||
            id: acc.address,
 | 
			
		||||
            id: `hooks-builder-req-info-${acc.address}`,
 | 
			
		||||
            command: "account_info",
 | 
			
		||||
            account: acc.address,
 | 
			
		||||
          })
 | 
			
		||||
@@ -244,16 +323,28 @@ const Accounts: FC<AccountProps> = (props) => {
 | 
			
		||||
          const balance = res?.account_data?.Balance as string;
 | 
			
		||||
          const sequence = res?.account_data?.Sequence as number;
 | 
			
		||||
          const accountToUpdate = state.accounts.find(
 | 
			
		||||
            (acc) => acc.address === address
 | 
			
		||||
            acc => acc.address === address
 | 
			
		||||
          );
 | 
			
		||||
          if (accountToUpdate) {
 | 
			
		||||
            accountToUpdate.xrp = balance;
 | 
			
		||||
            accountToUpdate.sequence = sequence;
 | 
			
		||||
            accountToUpdate.error = null;
 | 
			
		||||
          } else {
 | 
			
		||||
            const oldAccount = state.accounts.find(
 | 
			
		||||
              acc => acc.address === res?.account
 | 
			
		||||
            );
 | 
			
		||||
            if (oldAccount) {
 | 
			
		||||
              oldAccount.xrp = "0";
 | 
			
		||||
              oldAccount.error = {
 | 
			
		||||
                code: res?.error,
 | 
			
		||||
                message: res?.error_message,
 | 
			
		||||
              };
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
        const objectRequests = snap.accounts.map((acc) => {
 | 
			
		||||
        const objectRequests = snap.accounts.map(acc => {
 | 
			
		||||
          return snap.client?.send({
 | 
			
		||||
            id: `${acc.address}-hooks`,
 | 
			
		||||
            id: `hooks-builder-req-objects-${acc.address}`,
 | 
			
		||||
            command: "account_objects",
 | 
			
		||||
            account: acc.address,
 | 
			
		||||
          });
 | 
			
		||||
@@ -262,12 +353,13 @@ const Accounts: FC<AccountProps> = (props) => {
 | 
			
		||||
        objectResponses.forEach((res: any) => {
 | 
			
		||||
          const address = res?.account as string;
 | 
			
		||||
          const accountToUpdate = state.accounts.find(
 | 
			
		||||
            (acc) => acc.address === address
 | 
			
		||||
            acc => acc.address === address
 | 
			
		||||
          );
 | 
			
		||||
          if (accountToUpdate) {
 | 
			
		||||
            accountToUpdate.hooks = res.account_objects
 | 
			
		||||
              .filter((ac: any) => ac?.LedgerEntryType === "Hook")
 | 
			
		||||
              .map((oo: any) => oo.HookHash);
 | 
			
		||||
            accountToUpdate.hooks =
 | 
			
		||||
              res.account_objects
 | 
			
		||||
                .find((ac: any) => ac?.LedgerEntryType === "Hook")
 | 
			
		||||
                ?.Hooks?.map((oo: any) => oo.Hook.HookHash) || [];
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
@@ -276,7 +368,7 @@ const Accounts: FC<AccountProps> = (props) => {
 | 
			
		||||
    let fetchAccountInfoInterval: NodeJS.Timer;
 | 
			
		||||
    if (snap.clientStatus === "online") {
 | 
			
		||||
      fetchAccInfo();
 | 
			
		||||
      fetchAccountInfoInterval = setInterval(() => fetchAccInfo(), 2000);
 | 
			
		||||
      fetchAccountInfoInterval = setInterval(() => fetchAccInfo(), 10000);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
@@ -287,7 +379,7 @@ const Accounts: FC<AccountProps> = (props) => {
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, [snap.accounts, snap.clientStatus]);
 | 
			
		||||
  }, [snap.accounts.length, snap.clientStatus]);
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      as="div"
 | 
			
		||||
@@ -325,9 +417,7 @@ const Accounts: FC<AccountProps> = (props) => {
 | 
			
		||||
            <Wallet size="15px" /> <Text css={{ lineHeight: 1 }}>Accounts</Text>
 | 
			
		||||
          </Heading>
 | 
			
		||||
          <Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
 | 
			
		||||
            <Button ghost size="sm" onClick={() => addFaucetAccount(true)}>
 | 
			
		||||
              Create
 | 
			
		||||
            </Button>
 | 
			
		||||
            <ImportAccountDialog type="create" />
 | 
			
		||||
            <ImportAccountDialog />
 | 
			
		||||
          </Flex>
 | 
			
		||||
        </Flex>
 | 
			
		||||
@@ -338,14 +428,13 @@ const Accounts: FC<AccountProps> = (props) => {
 | 
			
		||||
            fontSize: "13px",
 | 
			
		||||
            wordWrap: "break-word",
 | 
			
		||||
            fontWeight: "$body",
 | 
			
		||||
            fontFamily: "$monospace",
 | 
			
		||||
            gap: 0,
 | 
			
		||||
            height: "calc(100% - 52px)",
 | 
			
		||||
            flexWrap: "nowrap",
 | 
			
		||||
            overflowY: "auto",
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {snap.accounts.map((account) => (
 | 
			
		||||
          {snap.accounts.map(account => (
 | 
			
		||||
            <Flex
 | 
			
		||||
              column
 | 
			
		||||
              key={account.address + account.name}
 | 
			
		||||
@@ -376,45 +465,33 @@ const Accounts: FC<AccountProps> = (props) => {
 | 
			
		||||
                      wordBreak: "break-word",
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    {account.address} (
 | 
			
		||||
                    {Dinero({
 | 
			
		||||
                      amount: Number(account?.xrp || "0"),
 | 
			
		||||
                      precision: 6,
 | 
			
		||||
                    })
 | 
			
		||||
                      .toUnit()
 | 
			
		||||
                      .toLocaleString(undefined, {
 | 
			
		||||
                        style: "currency",
 | 
			
		||||
                        currency: "XRP",
 | 
			
		||||
                        currencyDisplay: "name",
 | 
			
		||||
                      })}
 | 
			
		||||
                    )
 | 
			
		||||
                    {account.address}{" "}
 | 
			
		||||
                    {!account?.error ? (
 | 
			
		||||
                      `(${Dinero({
 | 
			
		||||
                        amount: Number(account?.xrp || "0"),
 | 
			
		||||
                        precision: 6,
 | 
			
		||||
                      })
 | 
			
		||||
                        .toUnit()
 | 
			
		||||
                        .toLocaleString(undefined, {
 | 
			
		||||
                          style: "currency",
 | 
			
		||||
                          currency: "XRP",
 | 
			
		||||
                          currencyDisplay: "name",
 | 
			
		||||
                        })})`
 | 
			
		||||
                    ) : (
 | 
			
		||||
                      <Box css={{ color: "$red11" }}>
 | 
			
		||||
                        (Account not found, request funds to activate account)
 | 
			
		||||
                      </Box>
 | 
			
		||||
                    )}
 | 
			
		||||
                  </Text>
 | 
			
		||||
                </Box>
 | 
			
		||||
                {!props.hideDeployBtn && (
 | 
			
		||||
                  <div
 | 
			
		||||
                    onClick={(e) => {
 | 
			
		||||
                      e.preventDefault();
 | 
			
		||||
                    className="hook-deploy-button"
 | 
			
		||||
                    onClick={e => {
 | 
			
		||||
                      e.stopPropagation();
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Button
 | 
			
		||||
                      css={{ ml: "auto" }}
 | 
			
		||||
                      size="xs"
 | 
			
		||||
                      uppercase
 | 
			
		||||
                      isLoading={account.isLoading}
 | 
			
		||||
                      disabled={
 | 
			
		||||
                        account.isLoading ||
 | 
			
		||||
                        !snap.files.filter((file) => file.compiledWatContent)
 | 
			
		||||
                          .length
 | 
			
		||||
                      }
 | 
			
		||||
                      variant="secondary"
 | 
			
		||||
                      onClick={(e) => {
 | 
			
		||||
                        e.stopPropagation();
 | 
			
		||||
                        deployHook(account);
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      Deploy
 | 
			
		||||
                    </Button>
 | 
			
		||||
                    <SetHookDialog accountAddress={account.address} />
 | 
			
		||||
                  </div>
 | 
			
		||||
                )}
 | 
			
		||||
              </Flex>
 | 
			
		||||
@@ -436,26 +513,71 @@ const Accounts: FC<AccountProps> = (props) => {
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ImportAccountDialog = () => {
 | 
			
		||||
  const [value, setValue] = useState("");
 | 
			
		||||
export const transactionsOptions = transactionsData.map(tx => ({
 | 
			
		||||
  value: tx.TransactionType,
 | 
			
		||||
  label: tx.TransactionType,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const ImportAccountDialog = ({
 | 
			
		||||
  type = "import",
 | 
			
		||||
}: {
 | 
			
		||||
  type?: "import" | "create";
 | 
			
		||||
}) => {
 | 
			
		||||
  const [secret, setSecret] = useState("");
 | 
			
		||||
  const [name, setName] = useState("");
 | 
			
		||||
 | 
			
		||||
  const btnText = type === "import" ? "Import" : "Create";
 | 
			
		||||
  const title = type === "import" ? "Import Account" : "Create Account";
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = async () => {
 | 
			
		||||
    if (type === "create") {
 | 
			
		||||
      const value = capitalize(name);
 | 
			
		||||
      await addFaucetAccount(value, true);
 | 
			
		||||
      setName("");
 | 
			
		||||
      setSecret("");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    importAccount(secret, name);
 | 
			
		||||
    setName("");
 | 
			
		||||
    setSecret("");
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog>
 | 
			
		||||
      <DialogTrigger asChild>
 | 
			
		||||
        <Button ghost size="sm">
 | 
			
		||||
          Import
 | 
			
		||||
          {btnText}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </DialogTrigger>
 | 
			
		||||
      <DialogContent>
 | 
			
		||||
        <DialogTitle>Import account</DialogTitle>
 | 
			
		||||
        <DialogDescription>
 | 
			
		||||
          <label>Add account secret</label>
 | 
			
		||||
          <Input
 | 
			
		||||
            name="secret"
 | 
			
		||||
            type="password"
 | 
			
		||||
            value={value}
 | 
			
		||||
            onChange={(e) => setValue(e.target.value)}
 | 
			
		||||
          />
 | 
			
		||||
        </DialogDescription>
 | 
			
		||||
      <DialogContent aria-describedby={undefined}>
 | 
			
		||||
        <DialogTitle css={{ mb: "$4" }}>{title}</DialogTitle>
 | 
			
		||||
        <Flex column>
 | 
			
		||||
          <Box css={{ mb: "$2" }}>
 | 
			
		||||
            <Label>
 | 
			
		||||
              Account name <Text muted>(optional)</Text>
 | 
			
		||||
            </Label>
 | 
			
		||||
            <Input
 | 
			
		||||
              name="name"
 | 
			
		||||
              type="text"
 | 
			
		||||
              autoComplete="off"
 | 
			
		||||
              autoCapitalize="on"
 | 
			
		||||
              value={name}
 | 
			
		||||
              onChange={e => setName(e.target.value)}
 | 
			
		||||
            />
 | 
			
		||||
          </Box>
 | 
			
		||||
          {type === "import" && (
 | 
			
		||||
            <Box>
 | 
			
		||||
              <Label>Account secret</Label>
 | 
			
		||||
              <Input
 | 
			
		||||
                required
 | 
			
		||||
                name="secret"
 | 
			
		||||
                type="password"
 | 
			
		||||
                autoComplete="new-password"
 | 
			
		||||
                value={secret}
 | 
			
		||||
                onChange={e => setSecret(e.target.value)}
 | 
			
		||||
              />
 | 
			
		||||
            </Box>
 | 
			
		||||
          )}
 | 
			
		||||
        </Flex>
 | 
			
		||||
 | 
			
		||||
        <Flex
 | 
			
		||||
          css={{
 | 
			
		||||
@@ -468,14 +590,8 @@ const ImportAccountDialog = () => {
 | 
			
		||||
            <Button outline>Cancel</Button>
 | 
			
		||||
          </DialogClose>
 | 
			
		||||
          <DialogClose asChild>
 | 
			
		||||
            <Button
 | 
			
		||||
              variant="primary"
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                importAccount(value);
 | 
			
		||||
                setValue("");
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              Import account
 | 
			
		||||
            <Button type="submit" variant="primary" onClick={handleSubmit}>
 | 
			
		||||
              {title}
 | 
			
		||||
            </Button>
 | 
			
		||||
          </DialogClose>
 | 
			
		||||
        </Flex>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										75
									
								
								components/AlertDialog/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								components/AlertDialog/index.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
import { FC, ReactNode } from "react";
 | 
			
		||||
import { proxy, useSnapshot } from "valtio";
 | 
			
		||||
import Button from "../Button";
 | 
			
		||||
import Flex from "../Flex";
 | 
			
		||||
import {
 | 
			
		||||
  AlertDialog,
 | 
			
		||||
  AlertDialogAction,
 | 
			
		||||
  AlertDialogCancel,
 | 
			
		||||
  AlertDialogContent,
 | 
			
		||||
  AlertDialogDescription,
 | 
			
		||||
  AlertDialogTitle,
 | 
			
		||||
} from "./primitive";
 | 
			
		||||
 | 
			
		||||
export interface AlertState {
 | 
			
		||||
  isOpen: boolean;
 | 
			
		||||
  title?: string;
 | 
			
		||||
  body?: ReactNode;
 | 
			
		||||
  cancelText?: string;
 | 
			
		||||
  confirmText?: string;
 | 
			
		||||
  confirmPrefix?: ReactNode;
 | 
			
		||||
  onConfirm?: () => any;
 | 
			
		||||
  onCancel?: () => any;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const alertState = proxy<AlertState>({
 | 
			
		||||
  isOpen: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const Alert: FC = () => {
 | 
			
		||||
  const {
 | 
			
		||||
    title = "Are you sure?",
 | 
			
		||||
    isOpen,
 | 
			
		||||
    body,
 | 
			
		||||
    cancelText,
 | 
			
		||||
    confirmText = "Ok",
 | 
			
		||||
    confirmPrefix,
 | 
			
		||||
    onCancel,
 | 
			
		||||
    onConfirm,
 | 
			
		||||
  } = useSnapshot(alertState);
 | 
			
		||||
  return (
 | 
			
		||||
    <AlertDialog
 | 
			
		||||
      open={isOpen}
 | 
			
		||||
      onOpenChange={value => (alertState.isOpen = value)}
 | 
			
		||||
    >
 | 
			
		||||
      <AlertDialogContent>
 | 
			
		||||
        <AlertDialogTitle>{title}</AlertDialogTitle>
 | 
			
		||||
        <AlertDialogDescription>{body}</AlertDialogDescription>
 | 
			
		||||
        <Flex css={{ justifyContent: "flex-end", gap: "$3" }}>
 | 
			
		||||
          {(cancelText || onCancel) && (
 | 
			
		||||
            <AlertDialogCancel asChild>
 | 
			
		||||
              <Button css={{ minWidth: "$16" }} outline onClick={onCancel}>
 | 
			
		||||
                {cancelText || "Cancel"}
 | 
			
		||||
              </Button>
 | 
			
		||||
            </AlertDialogCancel>
 | 
			
		||||
          )}
 | 
			
		||||
          <AlertDialogAction asChild>
 | 
			
		||||
            <Button
 | 
			
		||||
              css={{ minWidth: "$16" }}
 | 
			
		||||
              variant="primary"
 | 
			
		||||
              onClick={async () => {
 | 
			
		||||
                await onConfirm?.();
 | 
			
		||||
                alertState.isOpen = false;
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              {confirmPrefix}
 | 
			
		||||
              {confirmText}
 | 
			
		||||
            </Button>
 | 
			
		||||
          </AlertDialogAction>
 | 
			
		||||
        </Flex>
 | 
			
		||||
      </AlertDialogContent>
 | 
			
		||||
    </AlertDialog>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Alert;
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { blackA } from "@radix-ui/colors";
 | 
			
		||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
 | 
			
		||||
import { styled, keyframes } from "../stitches.config";
 | 
			
		||||
import { styled, keyframes } from "../../stitches.config";
 | 
			
		||||
 | 
			
		||||
const overlayShow = keyframes({
 | 
			
		||||
  "0%": { opacity: 0 },
 | 
			
		||||
@@ -75,7 +75,7 @@ const StyledDescription = styled(AlertDialogPrimitive.Description, {
 | 
			
		||||
  marginBottom: 20,
 | 
			
		||||
  color: "$mauve11",
 | 
			
		||||
  lineHeight: 1.5,
 | 
			
		||||
  fontSize: "$sm",
 | 
			
		||||
  fontSize: "$md",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Exports
 | 
			
		||||
@@ -87,7 +87,8 @@ export const StyledButton = styled("button", {
 | 
			
		||||
          boxShadow: "inset 0 0 0 1px $colors$mauve11",
 | 
			
		||||
        },
 | 
			
		||||
        "&:focus": {
 | 
			
		||||
          boxShadow: "inset 0 0 0 1px $colors$mauve12, inset 0 0 0 2px $colors$mauve12",
 | 
			
		||||
          boxShadow:
 | 
			
		||||
            "inset 0 0 0 1px $colors$mauve12, inset 0 0 0 2px $colors$mauve12",
 | 
			
		||||
        },
 | 
			
		||||
        '&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
 | 
			
		||||
          {
 | 
			
		||||
@@ -141,12 +142,45 @@ export const StyledButton = styled("button", {
 | 
			
		||||
            boxShadow: "inset 0 0 0 1px $colors$purple8",
 | 
			
		||||
          },
 | 
			
		||||
      },
 | 
			
		||||
      destroy: {
 | 
			
		||||
        backgroundColor: `$red9`,
 | 
			
		||||
        boxShadow: "inset 0 0 0 1px $colors$red9",
 | 
			
		||||
        color: "$white",
 | 
			
		||||
        "@hover": {
 | 
			
		||||
          "&:hover": {
 | 
			
		||||
            backgroundColor: "$red10",
 | 
			
		||||
            boxShadow: "inset 0 0 0 1px $colors$red11",
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        "&:active": {
 | 
			
		||||
          backgroundColor: "$red8",
 | 
			
		||||
          boxShadow: "inset 0 0 0 1px $colors$red8",
 | 
			
		||||
        },
 | 
			
		||||
        "&:focus": {
 | 
			
		||||
          boxShadow: "inset 0 0 0 2px $colors$red12",
 | 
			
		||||
        },
 | 
			
		||||
        '&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
 | 
			
		||||
          {
 | 
			
		||||
            backgroundColor: "$mauve4",
 | 
			
		||||
            boxShadow: "inset 0 0 0 1px $colors$red8",
 | 
			
		||||
          },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    muted: {
 | 
			
		||||
      true: {
 | 
			
		||||
        color: "$textMuted",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    isDisabled: {
 | 
			
		||||
      true: {
 | 
			
		||||
        opacity: 0.6,
 | 
			
		||||
        // pointerEvents: "none",
 | 
			
		||||
        cursor: "auto",
 | 
			
		||||
        "&:hover": {
 | 
			
		||||
          boxShadow: "inherit",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    outline: {
 | 
			
		||||
      true: {
 | 
			
		||||
        backgroundColor: "transparent",
 | 
			
		||||
@@ -236,16 +270,21 @@ export const StyledButton = styled("button", {
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const CustomButton: React.FC<React.ComponentProps<typeof StyledButton> & { as?: string }> =
 | 
			
		||||
  React.forwardRef(({ children, as = "button", ...rest }, ref) => (
 | 
			
		||||
    // @ts-expect-error
 | 
			
		||||
    <StyledButton {...rest} ref={ref} as={as}>
 | 
			
		||||
      <Flex as="span" css={{ gap: "$2", alignItems: "center" }} className="button-content">
 | 
			
		||||
        {children}
 | 
			
		||||
      </Flex>
 | 
			
		||||
      {rest.isLoading && <Spinner css={{ position: "absolute" }} />}
 | 
			
		||||
    </StyledButton>
 | 
			
		||||
  ));
 | 
			
		||||
const CustomButton: React.FC<
 | 
			
		||||
  React.ComponentProps<typeof StyledButton> & { as?: string }
 | 
			
		||||
> = React.forwardRef(({ children, as = "button", ...rest }, ref) => (
 | 
			
		||||
  // @ts-expect-error
 | 
			
		||||
  <StyledButton {...rest} ref={ref} as={as}>
 | 
			
		||||
    <Flex
 | 
			
		||||
      as="span"
 | 
			
		||||
      css={{ gap: "$2", alignItems: "center" }}
 | 
			
		||||
      className="button-content"
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
    </Flex>
 | 
			
		||||
    {rest.isLoading && <Spinner css={{ position: "absolute" }} />}
 | 
			
		||||
  </StyledButton>
 | 
			
		||||
));
 | 
			
		||||
 | 
			
		||||
CustomButton.displayName = "CustomButton";
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										135
									
								
								components/ContextMenu/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								components/ContextMenu/index.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,135 @@
 | 
			
		||||
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;
 | 
			
		||||
							
								
								
									
										107
									
								
								components/ContextMenu/primitive.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								components/ContextMenu/primitive.tsx
									
									
									
									
									
										Normal 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;
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
import { useCallback, useEffect } from "react";
 | 
			
		||||
import { useEffect } from "react";
 | 
			
		||||
import ReconnectingWebSocket, { CloseEvent } from "reconnecting-websocket";
 | 
			
		||||
import { proxy, ref, useSnapshot } from "valtio";
 | 
			
		||||
import { subscribeKey } from "valtio/utils";
 | 
			
		||||
import { Select } from ".";
 | 
			
		||||
import state, { ILog } from "../state";
 | 
			
		||||
import state, { ILog, transactionsState } from "../state";
 | 
			
		||||
import { extractJSON } from "../utils/json";
 | 
			
		||||
import LogBox from "./LogBox";
 | 
			
		||||
 | 
			
		||||
@@ -10,17 +12,99 @@ interface ISelect<T = string> {
 | 
			
		||||
  value: T;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const streamState = proxy({
 | 
			
		||||
export interface IStreamState {
 | 
			
		||||
  selectedAccount: ISelect | null;
 | 
			
		||||
  status: "idle" | "opened" | "closed";
 | 
			
		||||
  statusChangeTimestamp?: number;
 | 
			
		||||
  logs: ILog[];
 | 
			
		||||
  socket?: ReconnectingWebSocket;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const streamState = proxy<IStreamState>({
 | 
			
		||||
  selectedAccount: null as ISelect | null,
 | 
			
		||||
  status: "idle",
 | 
			
		||||
  logs: [] as ILog[],
 | 
			
		||||
  socket: undefined as WebSocket | undefined,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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 DebugStream = () => {
 | 
			
		||||
  const { selectedAccount, logs, socket } = useSnapshot(streamState);
 | 
			
		||||
  const { selectedAccount, logs } = useSnapshot(streamState);
 | 
			
		||||
  const { activeHeader: activeTxTab } = useSnapshot(transactionsState);
 | 
			
		||||
  const { accounts } = useSnapshot(state);
 | 
			
		||||
 | 
			
		||||
  const accountOptions = accounts.map(acc => ({
 | 
			
		||||
  const accountOptions = accounts.map((acc) => ({
 | 
			
		||||
    label: acc.name,
 | 
			
		||||
    value: acc.address,
 | 
			
		||||
  }));
 | 
			
		||||
@@ -33,106 +117,80 @@ const DebugStream = () => {
 | 
			
		||||
        options={accountOptions}
 | 
			
		||||
        hideSelectedOptions
 | 
			
		||||
        value={selectedAccount}
 | 
			
		||||
        onChange={acc => (streamState.selectedAccount = acc as any)}
 | 
			
		||||
        onChange={(acc) => {
 | 
			
		||||
          streamState.socket?.close(
 | 
			
		||||
            4999,
 | 
			
		||||
            "Old connection closed because user switched account"
 | 
			
		||||
          );
 | 
			
		||||
          streamState.selectedAccount = acc as any;
 | 
			
		||||
        }}
 | 
			
		||||
        css={{ width: "100%" }}
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const prepareLog = useCallback((str: any): ILog => {
 | 
			
		||||
    if (typeof str !== "string") throw Error("Unrecognized debug log stream!");
 | 
			
		||||
 | 
			
		||||
    const match = str.match(/([\s\S]+(?:UTC|ISO|GMT[+|-]\d+))\ ?([\s\S]*)/m);
 | 
			
		||||
    const [_, tm, msg] = match || [];
 | 
			
		||||
 | 
			
		||||
    const extracted = extractJSON(msg);
 | 
			
		||||
    const timestamp = isNaN(Date.parse(tm || ""))
 | 
			
		||||
      ? tm
 | 
			
		||||
      : new Date(tm).toLocaleTimeString();
 | 
			
		||||
 | 
			
		||||
    const message = !extracted
 | 
			
		||||
      ? msg
 | 
			
		||||
      : msg.slice(0, extracted.start) + msg.slice(extracted.end + 1);
 | 
			
		||||
 | 
			
		||||
    const jsonData = extracted
 | 
			
		||||
      ? JSON.stringify(extracted.result, null, 2)
 | 
			
		||||
      : undefined;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      type: "log",
 | 
			
		||||
      message,
 | 
			
		||||
      timestamp,
 | 
			
		||||
      jsonData,
 | 
			
		||||
      defaultCollapsed: true,
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const account = selectedAccount?.value;
 | 
			
		||||
    if (account && (!socket || !socket.url.endsWith(account))) {
 | 
			
		||||
      socket?.close();
 | 
			
		||||
      streamState.socket = ref(
 | 
			
		||||
        new WebSocket(
 | 
			
		||||
          `wss://hooks-testnet-debugstream.xrpl-labs.com/${account}`
 | 
			
		||||
        )
 | 
			
		||||
      );
 | 
			
		||||
    } else if (!account && socket) {
 | 
			
		||||
      socket.close();
 | 
			
		||||
      streamState.socket = undefined;
 | 
			
		||||
    }
 | 
			
		||||
  }, [selectedAccount?.value, socket]);
 | 
			
		||||
    const account = transactionsState.transactions.find(
 | 
			
		||||
      (tx) => tx.header === activeTxTab
 | 
			
		||||
    )?.state.selectedAccount;
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const account = selectedAccount?.value;
 | 
			
		||||
    const socket = streamState.socket;
 | 
			
		||||
    if (!socket) return;
 | 
			
		||||
    if (account && account.value !== streamState.selectedAccount?.value)
 | 
			
		||||
      streamState.selectedAccount = account;
 | 
			
		||||
  }, [activeTxTab]);
 | 
			
		||||
 | 
			
		||||
    const onOpen = () => {
 | 
			
		||||
      streamState.logs = [];
 | 
			
		||||
      streamState.logs.push({
 | 
			
		||||
        type: "success",
 | 
			
		||||
        message: `Debug stream opened for account ${account}`,
 | 
			
		||||
      });
 | 
			
		||||
    };
 | 
			
		||||
    const onError = () => {
 | 
			
		||||
      streamState.logs.push({
 | 
			
		||||
        type: "error",
 | 
			
		||||
        message: "Something went wrong! Check your connection and try again.",
 | 
			
		||||
      });
 | 
			
		||||
    };
 | 
			
		||||
    const onClose = (e: CloseEvent) => {
 | 
			
		||||
      streamState.logs.push({
 | 
			
		||||
        type: "error",
 | 
			
		||||
        message: `Connection was closed. [code: ${e.code}]`,
 | 
			
		||||
      });
 | 
			
		||||
      streamState.selectedAccount = null;
 | 
			
		||||
    };
 | 
			
		||||
    const onMessage = (event: any) => {
 | 
			
		||||
      if (!event.data) return;
 | 
			
		||||
      streamState.logs.push(prepareLog(event.data));
 | 
			
		||||
    };
 | 
			
		||||
  const clearLog = () => {
 | 
			
		||||
    streamState.logs = [];
 | 
			
		||||
    streamState.statusChangeTimestamp = Date.now();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
    socket.addEventListener("open", onOpen);
 | 
			
		||||
    socket.addEventListener("close", onClose);
 | 
			
		||||
    socket.addEventListener("error", onError);
 | 
			
		||||
    socket.addEventListener("message", onMessage);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      socket.removeEventListener("open", onOpen);
 | 
			
		||||
      socket.removeEventListener("close", onClose);
 | 
			
		||||
      socket.removeEventListener("message", onMessage);
 | 
			
		||||
      socket.removeEventListener("error", onError);
 | 
			
		||||
    };
 | 
			
		||||
  }, [prepareLog, selectedAccount?.value, socket]);
 | 
			
		||||
  return (
 | 
			
		||||
    <LogBox
 | 
			
		||||
      enhanced
 | 
			
		||||
      renderNav={renderNav}
 | 
			
		||||
      title="Debug stream"
 | 
			
		||||
      logs={logs}
 | 
			
		||||
      clearLog={() => (streamState.logs = [])}
 | 
			
		||||
      clearLog={clearLog}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default DebugStream;
 | 
			
		||||
 | 
			
		||||
export const pushLog = (
 | 
			
		||||
  str: any,
 | 
			
		||||
  opts: Partial<Pick<ILog, "type">> = {}
 | 
			
		||||
): ILog | undefined => {
 | 
			
		||||
  if (!str) return;
 | 
			
		||||
  if (typeof str !== "string") throw Error("Unrecognized debug log stream!");
 | 
			
		||||
 | 
			
		||||
  const match = str.match(/([\s\S]+(?:UTC|ISO|GMT[+|-]\d+))?\ ?([\s\S]*)/m);
 | 
			
		||||
  const [_, tm, msg] = match || [];
 | 
			
		||||
 | 
			
		||||
  const timestamp = Date.parse(tm || "") || undefined;
 | 
			
		||||
  const timestring = !timestamp ? tm : new Date(timestamp).toLocaleTimeString();
 | 
			
		||||
 | 
			
		||||
  const extracted = extractJSON(msg);
 | 
			
		||||
  const message = !extracted
 | 
			
		||||
    ? msg
 | 
			
		||||
    : msg.slice(0, extracted.start) + msg.slice(extracted.end + 1);
 | 
			
		||||
 | 
			
		||||
  const jsonData = extracted
 | 
			
		||||
    ? JSON.stringify(extracted.result, null, 2)
 | 
			
		||||
    : undefined;
 | 
			
		||||
 | 
			
		||||
  if (extracted?.result?.id?._Request?.includes("hooks-builder-req")) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const { type = "log" } = opts;
 | 
			
		||||
  const log: ILog = {
 | 
			
		||||
    type,
 | 
			
		||||
    message,
 | 
			
		||||
    timestring,
 | 
			
		||||
    jsonData,
 | 
			
		||||
    defaultCollapsed: true,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  if (log) streamState.logs.push(log);
 | 
			
		||||
  return log;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import React, { useRef, useState } from "react";
 | 
			
		||||
import { useSnapshot, ref } from "valtio";
 | 
			
		||||
import Editor, { loader } from "@monaco-editor/react";
 | 
			
		||||
import type monaco from "monaco-editor";
 | 
			
		||||
import React, { useState } from "react";
 | 
			
		||||
import { useSnapshot } from "valtio";
 | 
			
		||||
 | 
			
		||||
import { useTheme } from "next-themes";
 | 
			
		||||
import { useRouter } from "next/router";
 | 
			
		||||
import NextLink from "next/link";
 | 
			
		||||
@@ -10,31 +9,36 @@ import filesize from "filesize";
 | 
			
		||||
 | 
			
		||||
import Box from "./Box";
 | 
			
		||||
import Container from "./Container";
 | 
			
		||||
import dark from "../theme/editor/amy.json";
 | 
			
		||||
import light from "../theme/editor/xcode_default.json";
 | 
			
		||||
import state from "../state";
 | 
			
		||||
import wat from "../utils/wat-highlight";
 | 
			
		||||
 | 
			
		||||
import EditorNavigation from "./EditorNavigation";
 | 
			
		||||
import { Button, Text, Link, Flex } from ".";
 | 
			
		||||
 | 
			
		||||
loader.config({
 | 
			
		||||
  paths: {
 | 
			
		||||
    vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
import { Button, Text, Link, Flex, Tabs, Tab } from ".";
 | 
			
		||||
import Monaco from "./Monaco";
 | 
			
		||||
 | 
			
		||||
const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024];
 | 
			
		||||
 | 
			
		||||
const DeployEditor = () => {
 | 
			
		||||
  const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
 | 
			
		||||
  const snap = useSnapshot(state);
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
  const { theme } = useTheme();
 | 
			
		||||
 | 
			
		||||
  const [showContent, setShowContent] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const activeFile = snap.files[snap.active];
 | 
			
		||||
  const compiledFiles = snap.files.filter(file => file.compiledContent);
 | 
			
		||||
  const activeFile = compiledFiles[snap.activeWat];
 | 
			
		||||
 | 
			
		||||
  const renderNav = () => (
 | 
			
		||||
    <Tabs
 | 
			
		||||
      activeIndex={snap.activeWat}
 | 
			
		||||
      onChangeActive={idx => (state.activeWat = idx)}
 | 
			
		||||
    >
 | 
			
		||||
      {compiledFiles.map((file, index) => {
 | 
			
		||||
        return <Tab key={file.name} header={`${file.name}.wat`} />;
 | 
			
		||||
      })}
 | 
			
		||||
    </Tabs>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const compiledSize = activeFile?.compiledContent?.byteLength || 0;
 | 
			
		||||
  const color =
 | 
			
		||||
    compiledSize > FILESIZE_BREAKPOINTS[1]
 | 
			
		||||
@@ -43,6 +47,10 @@ const DeployEditor = () => {
 | 
			
		||||
      ? "$warning"
 | 
			
		||||
      : "$success";
 | 
			
		||||
 | 
			
		||||
  const isContentChanged =
 | 
			
		||||
    activeFile && activeFile.compiledValueSnapshot !== activeFile.content;
 | 
			
		||||
  // const hasDeployErrors = activeFile && activeFile.containsErrors;
 | 
			
		||||
 | 
			
		||||
  const CompiledStatView = activeFile && (
 | 
			
		||||
    <Flex
 | 
			
		||||
      column
 | 
			
		||||
@@ -60,15 +68,30 @@ const DeployEditor = () => {
 | 
			
		||||
        {activeFile?.lastCompiled && (
 | 
			
		||||
          <ReactTimeAgo date={activeFile.lastCompiled} locale="en-US" />
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        {activeFile.compiledContent?.byteLength && (
 | 
			
		||||
          <Text css={{ ml: "$2", color }}>
 | 
			
		||||
            ({filesize(activeFile.compiledContent.byteLength)})
 | 
			
		||||
          </Text>
 | 
			
		||||
        )}
 | 
			
		||||
      </Flex>
 | 
			
		||||
      {activeFile.compiledContent?.byteLength &&
 | 
			
		||||
        activeFile.compiledContent?.byteLength >= 64000 && (
 | 
			
		||||
          <Flex css={{ flexDirection: "column", py: "$3", pb: "$1" }}>
 | 
			
		||||
            <Text css={{ ml: "$2", color: "$error" }}>
 | 
			
		||||
              File size is larger than 64kB, cannot set hook!
 | 
			
		||||
            </Text>
 | 
			
		||||
          </Flex>
 | 
			
		||||
        )}
 | 
			
		||||
      <Button variant="link" onClick={() => setShowContent(true)}>
 | 
			
		||||
        View as WAT-file
 | 
			
		||||
      </Button>
 | 
			
		||||
      {isContentChanged && (
 | 
			
		||||
        <Text warning>
 | 
			
		||||
          File contents were changed after last compile, compile again to
 | 
			
		||||
          incorporate your latest changes in the build.
 | 
			
		||||
        </Text>
 | 
			
		||||
      )}
 | 
			
		||||
    </Flex>
 | 
			
		||||
  );
 | 
			
		||||
  const NoContentView = !snap.loading && router.isReady && (
 | 
			
		||||
@@ -87,8 +110,9 @@ const DeployEditor = () => {
 | 
			
		||||
      </NextLink>
 | 
			
		||||
    </Text>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const isContent =
 | 
			
		||||
    snap.files?.filter((file) => file.compiledWatContent).length > 0 &&
 | 
			
		||||
    snap.files?.filter(file => file.compiledWatContent).length > 0 &&
 | 
			
		||||
    router.isReady;
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
@@ -101,7 +125,7 @@ const DeployEditor = () => {
 | 
			
		||||
        width: "100%",
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <EditorNavigation showWat />
 | 
			
		||||
      <EditorNavigation renderNav={renderNav} />
 | 
			
		||||
      <Container
 | 
			
		||||
        css={{
 | 
			
		||||
          display: "flex",
 | 
			
		||||
@@ -115,32 +139,38 @@ const DeployEditor = () => {
 | 
			
		||||
        ) : !showContent ? (
 | 
			
		||||
          CompiledStatView
 | 
			
		||||
        ) : (
 | 
			
		||||
          <Editor
 | 
			
		||||
          <Monaco
 | 
			
		||||
            className="hooks-editor"
 | 
			
		||||
            defaultLanguage={"wat"}
 | 
			
		||||
            language={"wat"}
 | 
			
		||||
            path={`file://tmp/c/${snap.files?.[snap.active]?.name}.wat`}
 | 
			
		||||
            value={snap.files?.[snap.active]?.compiledWatContent || ""}
 | 
			
		||||
            beforeMount={(monaco) => {
 | 
			
		||||
            path={`file://tmp/c/${activeFile?.name}.wat`}
 | 
			
		||||
            value={activeFile?.compiledWatContent || ""}
 | 
			
		||||
            beforeMount={monaco => {
 | 
			
		||||
              monaco.languages.register({ id: "wat" });
 | 
			
		||||
              monaco.languages.setLanguageConfiguration("wat", wat.config);
 | 
			
		||||
              monaco.languages.setMonarchTokensProvider("wat", wat.tokens);
 | 
			
		||||
              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;
 | 
			
		||||
            onMount={editor => {
 | 
			
		||||
              editor.updateOptions({
 | 
			
		||||
                glyphMargin: true,
 | 
			
		||||
                readOnly: true,
 | 
			
		||||
              });
 | 
			
		||||
            }}
 | 
			
		||||
            theme={theme === "dark" ? "dark" : "light"}
 | 
			
		||||
            overlay={
 | 
			
		||||
              <Flex
 | 
			
		||||
                css={{
 | 
			
		||||
                  m: "$1",
 | 
			
		||||
                  ml: "auto",
 | 
			
		||||
                  fontSize: "$sm",
 | 
			
		||||
                  color: "$textMuted",
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Link onClick={() => setShowContent(false)}>
 | 
			
		||||
                  Exit editor mode
 | 
			
		||||
                </Link>
 | 
			
		||||
              </Flex>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
      </Container>
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -6,21 +6,28 @@ import * as DialogPrimitive from "@radix-ui/react-dialog";
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
 | 
			
		||||
const overlayShow = keyframes({
 | 
			
		||||
  "0%": { opacity: 0 },
 | 
			
		||||
  "0%": { opacity: 0.01 },
 | 
			
		||||
  "100%": { opacity: 1 },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const contentShow = keyframes({
 | 
			
		||||
  "0%": { opacity: 0, transform: "translate(-50%, -48%) scale(.96)" },
 | 
			
		||||
  "100%": { opacity: 1, transform: "translate(-50%, -50%) scale(1)" },
 | 
			
		||||
  "0%": { opacity: 0.01 },
 | 
			
		||||
  "100%": { opacity: 1 },
 | 
			
		||||
});
 | 
			
		||||
const StyledOverlay = styled(DialogPrimitive.Overlay, {
 | 
			
		||||
  zIndex: 1000,
 | 
			
		||||
  zIndex: 10000,
 | 
			
		||||
  backgroundColor: blackA.blackA9,
 | 
			
		||||
  position: "fixed",
 | 
			
		||||
  inset: 0,
 | 
			
		||||
  top: 0,
 | 
			
		||||
  left: 0,
 | 
			
		||||
  right: 0,
 | 
			
		||||
  bottom: 0,
 | 
			
		||||
  display: "grid",
 | 
			
		||||
  placeItems: "center",
 | 
			
		||||
  overflowY: "auto",
 | 
			
		||||
  "@media (prefers-reduced-motion: no-preference)": {
 | 
			
		||||
    animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
 | 
			
		||||
    animation: `${overlayShow} 250ms cubic-bezier(0.16, 1, 0.3, 1)`,
 | 
			
		||||
  },
 | 
			
		||||
  ".dark &": {
 | 
			
		||||
    backgroundColor: blackA.blackA11,
 | 
			
		||||
@@ -32,15 +39,13 @@ const StyledContent = styled(DialogPrimitive.Content, {
 | 
			
		||||
  backgroundColor: "$mauve2",
 | 
			
		||||
  color: "$mauve12",
 | 
			
		||||
  borderRadius: "$md",
 | 
			
		||||
  position: "relative",
 | 
			
		||||
  mb: "15%",
 | 
			
		||||
  boxShadow:
 | 
			
		||||
    "0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)",
 | 
			
		||||
  position: "fixed",
 | 
			
		||||
  top: "50%",
 | 
			
		||||
  left: "50%",
 | 
			
		||||
  transform: "translate(-50%, -50%)",
 | 
			
		||||
  width: "90vw",
 | 
			
		||||
  maxWidth: "450px",
 | 
			
		||||
  maxHeight: "85vh",
 | 
			
		||||
  // maxHeight: "85vh",
 | 
			
		||||
  padding: 25,
 | 
			
		||||
  "@media (prefers-reduced-motion: no-preference)": {
 | 
			
		||||
    animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
 | 
			
		||||
@@ -55,10 +60,9 @@ const StyledContent = styled(DialogPrimitive.Content, {
 | 
			
		||||
 | 
			
		||||
const Content: React.FC<{ css?: Stiches.CSS }> = ({ css, children }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <StyledOverlay />
 | 
			
		||||
    <StyledOverlay>
 | 
			
		||||
      <StyledContent css={css}>{children}</StyledContent>
 | 
			
		||||
    </div>
 | 
			
		||||
    </StyledOverlay>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -83,3 +87,4 @@ export const DialogContent = Content;
 | 
			
		||||
export const DialogTitle = StyledTitle;
 | 
			
		||||
export const DialogDescription = StyledDescription;
 | 
			
		||||
export const DialogClose = DialogPrimitive.Close;
 | 
			
		||||
export const DialogPortal = DialogPrimitive.Portal;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,27 +1,7 @@
 | 
			
		||||
import { keyframes } from "@stitches/react";
 | 
			
		||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
 | 
			
		||||
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
 | 
			
		||||
const slideUpAndFade = keyframes({
 | 
			
		||||
  "0%": { opacity: 0, transform: "translateY(2px)" },
 | 
			
		||||
  "100%": { opacity: 1, transform: "translateY(0)" },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const slideRightAndFade = keyframes({
 | 
			
		||||
  "0%": { opacity: 0, transform: "translateX(-2px)" },
 | 
			
		||||
  "100%": { opacity: 1, transform: "translateX(0)" },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const slideDownAndFade = keyframes({
 | 
			
		||||
  "0%": { opacity: 0, transform: "translateY(-2px)" },
 | 
			
		||||
  "100%": { opacity: 1, transform: "translateY(0)" },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const slideLeftAndFade = keyframes({
 | 
			
		||||
  "0%": { opacity: 0, transform: "translateX(2px)" },
 | 
			
		||||
  "100%": { opacity: 1, transform: "translateX(0)" },
 | 
			
		||||
});
 | 
			
		||||
import { slideDownAndFade, slideLeftAndFade, slideRightAndFade, slideUpAndFade } from '../styles/keyframes';
 | 
			
		||||
 | 
			
		||||
const StyledContent = styled(DropdownMenuPrimitive.Content, {
 | 
			
		||||
  minWidth: 220,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,10 @@
 | 
			
		||||
import React, { useState, useEffect, useCallback } from "react";
 | 
			
		||||
import React, {
 | 
			
		||||
  useState,
 | 
			
		||||
  useEffect,
 | 
			
		||||
  useRef,
 | 
			
		||||
  ReactNode,
 | 
			
		||||
} from "react";
 | 
			
		||||
import {
 | 
			
		||||
  Plus,
 | 
			
		||||
  Share,
 | 
			
		||||
  DownloadSimple,
 | 
			
		||||
  Gear,
 | 
			
		||||
@@ -25,9 +29,9 @@ import {
 | 
			
		||||
import NewWindow from "react-new-window";
 | 
			
		||||
import { signOut, useSession } from "next-auth/react";
 | 
			
		||||
import { useSnapshot } from "valtio";
 | 
			
		||||
import toast from "react-hot-toast";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  createNewFile,
 | 
			
		||||
  syncToGist,
 | 
			
		||||
  updateEditorSettings,
 | 
			
		||||
  downloadAsZip,
 | 
			
		||||
@@ -46,217 +50,90 @@ import {
 | 
			
		||||
} from "./Dialog";
 | 
			
		||||
import Flex from "./Flex";
 | 
			
		||||
import Stack from "./Stack";
 | 
			
		||||
import Input from "./Input";
 | 
			
		||||
import Text from "./Text";
 | 
			
		||||
import toast from "react-hot-toast";
 | 
			
		||||
import {
 | 
			
		||||
  AlertDialog,
 | 
			
		||||
  AlertDialogContent,
 | 
			
		||||
  AlertDialogTitle,
 | 
			
		||||
  AlertDialogDescription,
 | 
			
		||||
  AlertDialogCancel,
 | 
			
		||||
  AlertDialogAction,
 | 
			
		||||
} from "./AlertDialog";
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
import { Input, Label } from "./Input";
 | 
			
		||||
import Tooltip from "./Tooltip";
 | 
			
		||||
import { showAlert } from "../state/actions/showAlert";
 | 
			
		||||
 | 
			
		||||
const ErrorText = styled(Text, {
 | 
			
		||||
  color: "$error",
 | 
			
		||||
  mt: "$1",
 | 
			
		||||
  display: "block",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
 | 
			
		||||
const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
 | 
			
		||||
  const snap = useSnapshot(state);
 | 
			
		||||
  const [createNewAlertOpen, setCreateNewAlertOpen] = useState(false);
 | 
			
		||||
  const [editorSettingsOpen, setEditorSettingsOpen] = useState(false);
 | 
			
		||||
  const [isNewfileDialogOpen, setIsNewfileDialogOpen] = useState(false);
 | 
			
		||||
  const [newfileError, setNewfileError] = useState<string | null>(null);
 | 
			
		||||
  const [filename, setFilename] = useState("");
 | 
			
		||||
  const { data: session, status } = useSession();
 | 
			
		||||
  const [popup, setPopUp] = useState(false);
 | 
			
		||||
  const [editorSettings, setEditorSettings] = useState(snap.editorSettings);
 | 
			
		||||
  
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (session && session.user && popup) {
 | 
			
		||||
      setPopUp(false);
 | 
			
		||||
    }
 | 
			
		||||
  }, [session, popup]);
 | 
			
		||||
 | 
			
		||||
  // when filename changes, reset error
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setNewfileError(null);
 | 
			
		||||
  }, [filename, setNewfileError]);
 | 
			
		||||
 | 
			
		||||
  const validateFilename = useCallback(
 | 
			
		||||
    (filename: string): { error: string | null } => {
 | 
			
		||||
      // check if filename already exists
 | 
			
		||||
      if (!filename) {
 | 
			
		||||
        return { error: "You need to add filename" };
 | 
			
		||||
      }
 | 
			
		||||
      if (snap.files.find((file) => file.name === filename)) {
 | 
			
		||||
        return { error: "Filename already exists." };
 | 
			
		||||
      }
 | 
			
		||||
  const showNewGistAlert = () => {
 | 
			
		||||
    showAlert("Are you sure?", {
 | 
			
		||||
      body: (
 | 
			
		||||
        <>
 | 
			
		||||
          This action will create new <strong>public</strong> Github Gist from
 | 
			
		||||
          your current saved files. You can delete gist anytime from your GitHub
 | 
			
		||||
          Gists page.
 | 
			
		||||
        </>
 | 
			
		||||
      ),
 | 
			
		||||
      cancelText: "Cancel",
 | 
			
		||||
      confirmText: "Create new Gist",
 | 
			
		||||
      confirmPrefix: <FilePlus size="15px" />,
 | 
			
		||||
      onConfirm: () => syncToGist(session, true),
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
      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;
 | 
			
		||||
  const scrollRef = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const containerRef = useRef<HTMLDivElement>(null);
 | 
			
		||||
  return (
 | 
			
		||||
    <Flex css={{ flexShrink: 0, gap: "$0" }}>
 | 
			
		||||
      <Flex
 | 
			
		||||
        id="kissa"
 | 
			
		||||
        ref={scrollRef}
 | 
			
		||||
        css={{
 | 
			
		||||
          overflowX: "scroll",
 | 
			
		||||
          overflowY: "hidden",
 | 
			
		||||
          py: "$3",
 | 
			
		||||
          pb: "$0",
 | 
			
		||||
          flex: 1,
 | 
			
		||||
          "&::-webkit-scrollbar": {
 | 
			
		||||
            height: 0,
 | 
			
		||||
            background: "transparent",
 | 
			
		||||
            height: "0.3em",
 | 
			
		||||
            background: "rgba(0,0,0,.0)",
 | 
			
		||||
          },
 | 
			
		||||
          "&::-webkit-scrollbar-gutter": "stable",
 | 
			
		||||
          "&::-webkit-scrollbar-thumb": {
 | 
			
		||||
            backgroundColor: "rgba(0,0,0,.2)",
 | 
			
		||||
            outline: "0px",
 | 
			
		||||
            borderRadius: "9999px",
 | 
			
		||||
          },
 | 
			
		||||
          scrollbarColor: "rgba(0,0,0,.2) rgba(0,0,0,0)",
 | 
			
		||||
          scrollbarGutter: "stable",
 | 
			
		||||
          scrollbarWidth: "thin",
 | 
			
		||||
          ".dark &": {
 | 
			
		||||
            "&::-webkit-scrollbar": {
 | 
			
		||||
              background: "rgba(0,0,0,.0)",
 | 
			
		||||
            },
 | 
			
		||||
            "&::-webkit-scrollbar-gutter": "stable",
 | 
			
		||||
            "&::-webkit-scrollbar-thumb": {
 | 
			
		||||
              backgroundColor: "rgba(255,255,255,.2)",
 | 
			
		||||
              outline: "0px",
 | 
			
		||||
              borderRadius: "9999px",
 | 
			
		||||
            },
 | 
			
		||||
            scrollbarColor: "rgba(255,255,255,.2) rgba(0,0,0,0)",
 | 
			
		||||
            scrollbarGutter: "stable",
 | 
			
		||||
            scrollbarWidth: "thin",
 | 
			
		||||
          },
 | 
			
		||||
        }}
 | 
			
		||||
        onWheelCapture={e => {
 | 
			
		||||
          if (scrollRef.current) {
 | 
			
		||||
            scrollRef.current.scrollLeft += e.deltaY;
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Container css={{ flex: 1 }}>
 | 
			
		||||
          <Stack
 | 
			
		||||
            css={{
 | 
			
		||||
              gap: "$3",
 | 
			
		||||
              flex: 1,
 | 
			
		||||
              flexWrap: "nowrap",
 | 
			
		||||
              marginBottom: "-1px",
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            {files &&
 | 
			
		||||
              files.length > 0 &&
 | 
			
		||||
              files.map((file, index) => {
 | 
			
		||||
                if (!file.compiledContent && showWat) {
 | 
			
		||||
                  return null;
 | 
			
		||||
                }
 | 
			
		||||
                return (
 | 
			
		||||
                  <Button
 | 
			
		||||
                    size="sm"
 | 
			
		||||
                    outline={
 | 
			
		||||
                      showWat ? snap.activeWat !== index : snap.active !== index
 | 
			
		||||
                    }
 | 
			
		||||
                    onClick={() => (state.active = index)}
 | 
			
		||||
                    key={file.name + index}
 | 
			
		||||
                    css={{
 | 
			
		||||
                      "&: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 css={{ flex: 1 }} ref={containerRef}>
 | 
			
		||||
          {renderNav?.()}
 | 
			
		||||
        </Container>
 | 
			
		||||
      </Flex>
 | 
			
		||||
      <Flex
 | 
			
		||||
@@ -367,44 +244,65 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
 | 
			
		||||
              },
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Button
 | 
			
		||||
              isLoading={snap.zipLoading}
 | 
			
		||||
              onClick={downloadAsZip}
 | 
			
		||||
              outline
 | 
			
		||||
              size="sm"
 | 
			
		||||
              css={{ alignItems: "center" }}
 | 
			
		||||
            <Tooltip content="Download as ZIP">
 | 
			
		||||
              <Button
 | 
			
		||||
                isLoading={snap.zipLoading}
 | 
			
		||||
                onClick={downloadAsZip}
 | 
			
		||||
                outline
 | 
			
		||||
                size="sm"
 | 
			
		||||
                css={{ alignItems: "center" }}
 | 
			
		||||
              >
 | 
			
		||||
                <DownloadSimple size="16px" />
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Tooltip>
 | 
			
		||||
            <Tooltip content="Copy share link to clipboard">
 | 
			
		||||
              <Button
 | 
			
		||||
                outline
 | 
			
		||||
                size="sm"
 | 
			
		||||
                css={{ alignItems: "center" }}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  navigator.clipboard.writeText(
 | 
			
		||||
                    `${window.location.origin}/develop/${snap.gistId}`
 | 
			
		||||
                  );
 | 
			
		||||
                  toast.success("Copied share link to clipboard!");
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Share size="16px" />
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Tooltip>
 | 
			
		||||
            <Tooltip
 | 
			
		||||
              content={
 | 
			
		||||
                session && session.user
 | 
			
		||||
                  ? snap.gistOwner === session?.user.username
 | 
			
		||||
                    ? "Sync to Gist"
 | 
			
		||||
                    : "Create as a new Gist"
 | 
			
		||||
                  : "You need to be logged in to sync with Gist"
 | 
			
		||||
              }
 | 
			
		||||
            >
 | 
			
		||||
              <DownloadSimple size="16px" />
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Button
 | 
			
		||||
              outline
 | 
			
		||||
              size="sm"
 | 
			
		||||
              css={{ alignItems: "center" }}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                navigator.clipboard.writeText(
 | 
			
		||||
                  `${window.location.origin}/develop/${snap.gistId}`
 | 
			
		||||
                );
 | 
			
		||||
                toast.success("Copied share link to clipboard!");
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Share size="16px" />
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Button
 | 
			
		||||
              outline
 | 
			
		||||
              size="sm"
 | 
			
		||||
              disabled={!session || !session.user}
 | 
			
		||||
              isLoading={snap.gistLoading}
 | 
			
		||||
              css={{ alignItems: "center" }}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                if (snap.gistOwner === session?.user.username) {
 | 
			
		||||
                  syncToGist(session);
 | 
			
		||||
                } else {
 | 
			
		||||
                  setCreateNewAlertOpen(true);
 | 
			
		||||
                }
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <CloudArrowUp size="16px" />
 | 
			
		||||
            </Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                outline
 | 
			
		||||
                size="sm"
 | 
			
		||||
                isDisabled={!session || !session.user}
 | 
			
		||||
                isLoading={snap.gistLoading}
 | 
			
		||||
                css={{ alignItems: "center" }}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  if (!session || !session.user) {
 | 
			
		||||
                    return;
 | 
			
		||||
                  }
 | 
			
		||||
                  if (snap.gistOwner === session?.user.username) {
 | 
			
		||||
                    syncToGist(session);
 | 
			
		||||
                  } else {
 | 
			
		||||
                    showNewGistAlert();
 | 
			
		||||
                  }
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                {snap.gistOwner === session?.user.username ? (
 | 
			
		||||
                  <CloudArrowUp size="16px" />
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <FilePlus size="16px" />
 | 
			
		||||
                )}
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Tooltip>
 | 
			
		||||
 | 
			
		||||
            <DropdownMenu>
 | 
			
		||||
              <DropdownMenuTrigger asChild>
 | 
			
		||||
@@ -444,7 +342,7 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
 | 
			
		||||
                <DropdownMenuItem
 | 
			
		||||
                  disabled={status !== "authenticated"}
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    setCreateNewAlertOpen(true);
 | 
			
		||||
                    showNewGistAlert();
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <FilePlus size="16px" /> Create as a new Gist
 | 
			
		||||
@@ -464,34 +362,6 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
 | 
			
		||||
          ) : null}
 | 
			
		||||
        </Container>
 | 
			
		||||
      </Flex>
 | 
			
		||||
      <AlertDialog
 | 
			
		||||
        open={createNewAlertOpen}
 | 
			
		||||
        onOpenChange={(value) => setCreateNewAlertOpen(value)}
 | 
			
		||||
      >
 | 
			
		||||
        <AlertDialogContent>
 | 
			
		||||
          <AlertDialogTitle>Are you sure?</AlertDialogTitle>
 | 
			
		||||
          <AlertDialogDescription>
 | 
			
		||||
            This action will create new <strong>public</strong> Github Gist from
 | 
			
		||||
            your current saved files. You can delete gist anytime from your
 | 
			
		||||
            GitHub Gists page.
 | 
			
		||||
          </AlertDialogDescription>
 | 
			
		||||
          <Flex css={{ justifyContent: "flex-end", gap: "$3" }}>
 | 
			
		||||
            <AlertDialogCancel asChild>
 | 
			
		||||
              <Button outline>Cancel</Button>
 | 
			
		||||
            </AlertDialogCancel>
 | 
			
		||||
            <AlertDialogAction asChild>
 | 
			
		||||
              <Button
 | 
			
		||||
                variant="primary"
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  syncToGist(session, true);
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <FilePlus size="15px" /> Create new Gist
 | 
			
		||||
              </Button>
 | 
			
		||||
            </AlertDialogAction>
 | 
			
		||||
          </Flex>
 | 
			
		||||
        </AlertDialogContent>
 | 
			
		||||
      </AlertDialog>
 | 
			
		||||
 | 
			
		||||
      <Dialog open={editorSettingsOpen} onOpenChange={setEditorSettingsOpen}>
 | 
			
		||||
        <DialogTrigger asChild>
 | 
			
		||||
@@ -502,13 +372,13 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
 | 
			
		||||
        <DialogContent>
 | 
			
		||||
          <DialogTitle>Editor settings</DialogTitle>
 | 
			
		||||
          <DialogDescription>
 | 
			
		||||
            <label>Tab size</label>
 | 
			
		||||
            <Label>Tab size</Label>
 | 
			
		||||
            <Input
 | 
			
		||||
              type="number"
 | 
			
		||||
              min="1"
 | 
			
		||||
              value={editorSettings.tabSize}
 | 
			
		||||
              onChange={(e) =>
 | 
			
		||||
                setEditorSettings((curr) => ({
 | 
			
		||||
              onChange={e =>
 | 
			
		||||
                setEditorSettings(curr => ({
 | 
			
		||||
                  ...curr,
 | 
			
		||||
                  tabSize: Number(e.target.value),
 | 
			
		||||
                }))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,13 @@
 | 
			
		||||
import React, { useEffect, useRef } from "react";
 | 
			
		||||
import { useSnapshot, ref } from "valtio";
 | 
			
		||||
import Editor, { loader } from "@monaco-editor/react";
 | 
			
		||||
import type monaco from "monaco-editor";
 | 
			
		||||
import { ArrowBendLeftUp } from "phosphor-react";
 | 
			
		||||
import { useTheme } from "next-themes";
 | 
			
		||||
import { useRouter } from "next/router";
 | 
			
		||||
import uniqBy from "lodash.uniqby";
 | 
			
		||||
 | 
			
		||||
import Box from "./Box";
 | 
			
		||||
import Container from "./Container";
 | 
			
		||||
import dark from "../theme/editor/amy.json";
 | 
			
		||||
import light from "../theme/editor/xcode_default.json";
 | 
			
		||||
import { saveFile } from "../state/actions";
 | 
			
		||||
import { createNewFile, saveFile } from "../state/actions";
 | 
			
		||||
import { apiHeaderFiles } from "../state/constants";
 | 
			
		||||
import state from "../state";
 | 
			
		||||
 | 
			
		||||
@@ -23,20 +19,24 @@ import { listen } from "@codingame/monaco-jsonrpc";
 | 
			
		||||
import ReconnectingWebSocket from "reconnecting-websocket";
 | 
			
		||||
 | 
			
		||||
import docs from "../xrpl-hooks-docs/docs";
 | 
			
		||||
import Monaco from "./Monaco";
 | 
			
		||||
import { saveAllFiles } from "../state/actions/saveFile";
 | 
			
		||||
import { Tab, Tabs } from "./Tabs";
 | 
			
		||||
import { renameFile } from "../state/actions/createNewFile";
 | 
			
		||||
 | 
			
		||||
loader.config({
 | 
			
		||||
  paths: {
 | 
			
		||||
    vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
 | 
			
		||||
  const currPath = editor.getModel()?.uri.path;
 | 
			
		||||
  if (apiHeaderFiles.find((h) => currPath?.endsWith(h))) {
 | 
			
		||||
    editor.updateOptions({ readOnly: true });
 | 
			
		||||
  } else {
 | 
			
		||||
    editor.updateOptions({ readOnly: false });
 | 
			
		||||
const checkWritable = (filename?: string): boolean => {
 | 
			
		||||
  if (apiHeaderFiles.find(file => file === filename)) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const validateWritability = (
 | 
			
		||||
  editor: monaco.editor.IStandaloneCodeEditor
 | 
			
		||||
) => {
 | 
			
		||||
  const filename = editor.getModel()?.uri.path.split("/").pop();
 | 
			
		||||
  const isWritable = checkWritable(filename);
 | 
			
		||||
  editor.updateOptions({ readOnly: !isWritable });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
let decorations: { [key: string]: string[] } = {};
 | 
			
		||||
@@ -45,18 +45,15 @@ const setMarkers = (monacoE: typeof monaco) => {
 | 
			
		||||
  // Get all the markers that are active at the moment,
 | 
			
		||||
  // Also if same error is there twice, we can show the content
 | 
			
		||||
  // only once (that's why we're using uniqBy)
 | 
			
		||||
  const markers = uniqBy(
 | 
			
		||||
    monacoE.editor
 | 
			
		||||
      .getModelMarkers({})
 | 
			
		||||
      // Filter out the markers that are hooks specific
 | 
			
		||||
      .filter(
 | 
			
		||||
        (marker) =>
 | 
			
		||||
          typeof marker?.code === "string" &&
 | 
			
		||||
          // Take only markers that starts with "hooks-"
 | 
			
		||||
          marker?.code?.includes("hooks-")
 | 
			
		||||
      ),
 | 
			
		||||
    "code"
 | 
			
		||||
  );
 | 
			
		||||
  const markers = monacoE.editor
 | 
			
		||||
    .getModelMarkers({})
 | 
			
		||||
    // Filter out the markers that are hooks specific
 | 
			
		||||
    .filter(
 | 
			
		||||
      marker =>
 | 
			
		||||
        typeof marker?.code === "string" &&
 | 
			
		||||
        // Take only markers that starts with "hooks-"
 | 
			
		||||
        marker?.code?.includes("hooks-")
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
  // Get the active model (aka active file you're editing)
 | 
			
		||||
  // const model = monacoE.editor?.getModel(
 | 
			
		||||
@@ -66,16 +63,16 @@ const setMarkers = (monacoE: typeof monaco) => {
 | 
			
		||||
  // Add decoration (aka extra hoverMessages) to markers in the
 | 
			
		||||
  // exact same range (location) where the markers are
 | 
			
		||||
  const models = monacoE.editor.getModels();
 | 
			
		||||
  models.forEach((model) => {
 | 
			
		||||
  models.forEach(model => {
 | 
			
		||||
    decorations[model.id] = model?.deltaDecorations(
 | 
			
		||||
      decorations?.[model.id] || [],
 | 
			
		||||
      markers
 | 
			
		||||
        .filter((marker) =>
 | 
			
		||||
        .filter(marker =>
 | 
			
		||||
          marker?.resource.path
 | 
			
		||||
            .split("/")
 | 
			
		||||
            .includes(`${state.files?.[state.active]?.name}`)
 | 
			
		||||
        )
 | 
			
		||||
        .map((marker) => ({
 | 
			
		||||
        .map(marker => ({
 | 
			
		||||
          range: new monacoE.Range(
 | 
			
		||||
            marker.startLineNumber,
 | 
			
		||||
            marker.startColumn,
 | 
			
		||||
@@ -123,6 +120,34 @@ const HooksEditor = () => {
 | 
			
		||||
      setMarkers(monacoRef.current);
 | 
			
		||||
    }
 | 
			
		||||
  }, [snap.active]);
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    return () => {
 | 
			
		||||
      saveAllFiles();
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const file = snap.files[snap.active];
 | 
			
		||||
 | 
			
		||||
  const renderNav = () => (
 | 
			
		||||
    <Tabs
 | 
			
		||||
      label="File"
 | 
			
		||||
      activeIndex={snap.active}
 | 
			
		||||
      onChangeActive={idx => (state.active = idx)}
 | 
			
		||||
      extensionRequired
 | 
			
		||||
      onCreateNewTab={createNewFile}
 | 
			
		||||
      onCloseTab={idx => state.files.splice(idx, 1)}
 | 
			
		||||
      onRenameTab={(idx, nwName, oldName = "") => renameFile(oldName, nwName)}
 | 
			
		||||
      headerExtraValidation={{
 | 
			
		||||
        regex: /^[A-Za-z0-9_-]+[.][A-Za-z0-9]{1,4}$/g,
 | 
			
		||||
        error:
 | 
			
		||||
          'Filename can contain only characters from a-z, A-Z, 0-9, "_" and "-"',
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {snap.files.map((file, index) => {
 | 
			
		||||
        return <Tab key={file.name} header={file.name} renameDisabled={!checkWritable(file.name)} />;
 | 
			
		||||
      })}
 | 
			
		||||
    </Tabs>
 | 
			
		||||
  );
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      css={{
 | 
			
		||||
@@ -135,18 +160,18 @@ const HooksEditor = () => {
 | 
			
		||||
        width: "100%",
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <EditorNavigation />
 | 
			
		||||
      <EditorNavigation renderNav={renderNav} />
 | 
			
		||||
      {snap.files.length > 0 && router.isReady ? (
 | 
			
		||||
        <Editor
 | 
			
		||||
          className="hooks-editor"
 | 
			
		||||
        <Monaco
 | 
			
		||||
          keepCurrentModel
 | 
			
		||||
          defaultLanguage={snap.files?.[snap.active]?.language}
 | 
			
		||||
          language={snap.files?.[snap.active]?.language}
 | 
			
		||||
          path={`file:///work/c/${snap.files?.[snap.active]?.name}`}
 | 
			
		||||
          defaultValue={snap.files?.[snap.active]?.content}
 | 
			
		||||
          beforeMount={(monaco) => {
 | 
			
		||||
          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) =>
 | 
			
		||||
              snap.files.forEach(file =>
 | 
			
		||||
                monaco.editor.createModel(
 | 
			
		||||
                  file.content,
 | 
			
		||||
                  file.language,
 | 
			
		||||
@@ -171,13 +196,13 @@ const HooksEditor = () => {
 | 
			
		||||
              // listen when the web socket is opened
 | 
			
		||||
              listen({
 | 
			
		||||
                webSocket: webSocket as WebSocket,
 | 
			
		||||
                onConnection: (connection) => {
 | 
			
		||||
                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);
 | 
			
		||||
@@ -187,7 +212,6 @@ const HooksEditor = () => {
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // // hook editor to global state
 | 
			
		||||
            // editor.updateOptions({
 | 
			
		||||
            //   minimap: {
 | 
			
		||||
            //     enabled: false,
 | 
			
		||||
@@ -196,10 +220,6 @@ const HooksEditor = () => {
 | 
			
		||||
            // });
 | 
			
		||||
            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) => {
 | 
			
		||||
@@ -226,6 +246,22 @@ const HooksEditor = () => {
 | 
			
		||||
              }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Hacky way to hide Peek menu
 | 
			
		||||
            editor.onContextMenu(e => {
 | 
			
		||||
              const host =
 | 
			
		||||
                document.querySelector<HTMLElement>(".shadow-root-host");
 | 
			
		||||
 | 
			
		||||
              const contextMenuItems =
 | 
			
		||||
                host?.shadowRoot?.querySelectorAll("li.action-item");
 | 
			
		||||
              contextMenuItems?.forEach(k => {
 | 
			
		||||
                // If menu item contains "Peek" lets hide it
 | 
			
		||||
                if (k.querySelector(".action-label")?.textContent === "Peek") {
 | 
			
		||||
                  // @ts-expect-error
 | 
			
		||||
                  k["style"].display = "none";
 | 
			
		||||
                }
 | 
			
		||||
              });
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            validateWritability(editor);
 | 
			
		||||
          }}
 | 
			
		||||
          theme={theme === "dark" ? "dark" : "light"}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,6 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
import * as LabelPrim from '@radix-ui/react-label';
 | 
			
		||||
 | 
			
		||||
export const Input = styled("input", {
 | 
			
		||||
  // Reset
 | 
			
		||||
@@ -58,6 +60,8 @@ export const Input = styled("input", {
 | 
			
		||||
  },
 | 
			
		||||
  "&:read-only": {
 | 
			
		||||
    backgroundColor: "$mauve2",
 | 
			
		||||
    color: "$text",
 | 
			
		||||
    opacity: 0.8,
 | 
			
		||||
    "&:focus": {
 | 
			
		||||
      boxShadow: "inset 0px 0px 0px 1px $colors$mauve7",
 | 
			
		||||
    },
 | 
			
		||||
@@ -148,4 +152,18 @@ export const Input = styled("input", {
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default Input;
 | 
			
		||||
// eslint-disable-next-line react/display-name
 | 
			
		||||
const ReffedInput = React.forwardRef<
 | 
			
		||||
  HTMLInputElement,
 | 
			
		||||
  React.ComponentProps<typeof Input>
 | 
			
		||||
>((props, ref) => <Input {...props} ref={ref} />);
 | 
			
		||||
 | 
			
		||||
export default ReffedInput;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const LabelRoot = (props: LabelPrim.LabelProps) => <LabelPrim.Root {...props} />
 | 
			
		||||
 | 
			
		||||
export const Label = styled(LabelRoot, {
 | 
			
		||||
  display: 'inline-block',
 | 
			
		||||
  mb: '$1'
 | 
			
		||||
})
 | 
			
		||||
@@ -1,5 +1,12 @@
 | 
			
		||||
import { useRef, useLayoutEffect, ReactNode, FC, useState, useCallback } from "react";
 | 
			
		||||
import { Notepad, Prohibit } from "phosphor-react";
 | 
			
		||||
import {
 | 
			
		||||
  useRef,
 | 
			
		||||
  useLayoutEffect,
 | 
			
		||||
  ReactNode,
 | 
			
		||||
  FC,
 | 
			
		||||
  useState,
 | 
			
		||||
  useCallback,
 | 
			
		||||
} from "react";
 | 
			
		||||
import { IconProps, Notepad, Prohibit } from "phosphor-react";
 | 
			
		||||
import useStayScrolled from "react-stay-scrolled";
 | 
			
		||||
import NextLink from "next/link";
 | 
			
		||||
 | 
			
		||||
@@ -17,9 +24,18 @@ interface ILogBox {
 | 
			
		||||
  logs: ILog[];
 | 
			
		||||
  renderNav?: () => ReactNode;
 | 
			
		||||
  enhanced?: boolean;
 | 
			
		||||
  Icon?: FC<IconProps>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const LogBox: FC<ILogBox> = ({ title, clearLog, logs, children, renderNav, enhanced }) => {
 | 
			
		||||
const LogBox: FC<ILogBox> = ({
 | 
			
		||||
  title,
 | 
			
		||||
  clearLog,
 | 
			
		||||
  logs,
 | 
			
		||||
  children,
 | 
			
		||||
  renderNav,
 | 
			
		||||
  enhanced,
 | 
			
		||||
  Icon = Notepad,
 | 
			
		||||
}) => {
 | 
			
		||||
  const logRef = useRef<HTMLPreElement>(null);
 | 
			
		||||
  const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
 | 
			
		||||
 | 
			
		||||
@@ -68,14 +84,14 @@ const LogBox: FC<ILogBox> = ({ title, clearLog, logs, children, renderNav, enhan
 | 
			
		||||
              gap: "$3",
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Notepad size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
 | 
			
		||||
            <Icon size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
 | 
			
		||||
          </Heading>
 | 
			
		||||
          <Flex
 | 
			
		||||
            row
 | 
			
		||||
            align="center"
 | 
			
		||||
            css={{
 | 
			
		||||
              width: "50%", // TODO make it max without breaking layout!
 | 
			
		||||
            }}
 | 
			
		||||
            // css={{
 | 
			
		||||
            //   maxWidth: "100%", // TODO make it max without breaking layout!
 | 
			
		||||
            // }}
 | 
			
		||||
          >
 | 
			
		||||
            {renderNav?.()}
 | 
			
		||||
          </Flex>
 | 
			
		||||
@@ -133,7 +149,7 @@ const LogBox: FC<ILogBox> = ({ title, clearLog, logs, children, renderNav, enhan
 | 
			
		||||
 | 
			
		||||
export const Log: FC<ILog> = ({
 | 
			
		||||
  type,
 | 
			
		||||
  timestamp: timestamp,
 | 
			
		||||
  timestring,
 | 
			
		||||
  message: _message,
 | 
			
		||||
  link,
 | 
			
		||||
  linkText,
 | 
			
		||||
@@ -172,8 +188,17 @@ export const Log: FC<ILog> = ({
 | 
			
		||||
    },
 | 
			
		||||
    [accounts]
 | 
			
		||||
  );
 | 
			
		||||
  _message = _message.trim().replace(/\n /gi, "\n");
 | 
			
		||||
  const message = enrichAccounts(_message);
 | 
			
		||||
 | 
			
		||||
  let message: ReactNode;
 | 
			
		||||
 | 
			
		||||
  if (typeof _message === "string") {
 | 
			
		||||
    _message = _message.trim().replace(/\n /gi, "\n");
 | 
			
		||||
    if (_message) message = enrichAccounts(_message);
 | 
			
		||||
    else message = <Text muted>{'""'}</Text>
 | 
			
		||||
  } else {
 | 
			
		||||
    message = _message;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const jsonData = enrichAccounts(_jsonData);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
@@ -183,7 +208,11 @@ export const Log: FC<ILog> = ({
 | 
			
		||||
        activeAccountAddress={dialogAccount}
 | 
			
		||||
      />
 | 
			
		||||
      <LogText variant={type}>
 | 
			
		||||
        {timestamp && <Text muted monospace>{timestamp} </Text>}
 | 
			
		||||
        {timestring && (
 | 
			
		||||
          <Text muted monospace>
 | 
			
		||||
            {timestring}{" "}
 | 
			
		||||
          </Text>
 | 
			
		||||
        )}
 | 
			
		||||
        <Pre>{message} </Pre>
 | 
			
		||||
        {link && (
 | 
			
		||||
          <NextLink href={link} shallow passHref>
 | 
			
		||||
@@ -197,6 +226,7 @@ export const Log: FC<ILog> = ({
 | 
			
		||||
        )}
 | 
			
		||||
        {expanded && jsonData && <Pre block>{jsonData}</Pre>}
 | 
			
		||||
      </LogText>
 | 
			
		||||
      <br />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										75
									
								
								components/Monaco.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								components/Monaco.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
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;
 | 
			
		||||
@@ -28,6 +28,22 @@ import {
 | 
			
		||||
} from "./Dialog";
 | 
			
		||||
import PanelBox from "./PanelBox";
 | 
			
		||||
import { templateFileIds } from "../state/constants";
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
 | 
			
		||||
const ImageWrapper = styled(Flex, {
 | 
			
		||||
  position: "relative",
 | 
			
		||||
  mt: "$2",
 | 
			
		||||
  mb: "$10",
 | 
			
		||||
  svg: {
 | 
			
		||||
    // fill: "red",
 | 
			
		||||
    ".angle": {
 | 
			
		||||
      fill: "$text",
 | 
			
		||||
    },
 | 
			
		||||
    ":not(.angle)": {
 | 
			
		||||
      stroke: "$text",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const Navigation = () => {
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
@@ -91,7 +107,7 @@ const Navigation = () => {
 | 
			
		||||
                <Text
 | 
			
		||||
                  css={{ fontSize: "$xs", color: "$mauve10", lineHeight: 1 }}
 | 
			
		||||
                >
 | 
			
		||||
                  {snap.files.length > 0 ? "Gist: " : "Playground"}
 | 
			
		||||
                  {snap.files.length > 0 ? "Gist: " : "Builder"}
 | 
			
		||||
                  {snap.files.length > 0 && (
 | 
			
		||||
                    <Link
 | 
			
		||||
                      href={`https://gist.github.com/${snap.gistOwner || ""}/${
 | 
			
		||||
@@ -128,19 +144,20 @@ const Navigation = () => {
 | 
			
		||||
                </DialogTrigger>
 | 
			
		||||
                <DialogContent
 | 
			
		||||
                  css={{
 | 
			
		||||
                    maxWidth: "100%",
 | 
			
		||||
                    display: "flex",
 | 
			
		||||
                    maxWidth: "1080px",
 | 
			
		||||
                    width: "80vw",
 | 
			
		||||
                    height: "80%",
 | 
			
		||||
                    maxHeight: "80%",
 | 
			
		||||
                    backgroundColor: "$mauve1 !important",
 | 
			
		||||
                    overflowY: "auto",
 | 
			
		||||
                    background: "black",
 | 
			
		||||
                    p: 0,
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Flex
 | 
			
		||||
                    css={{
 | 
			
		||||
                      flexDirection: "column",
 | 
			
		||||
                      flex: 1,
 | 
			
		||||
                      height: "auto",
 | 
			
		||||
                      height: "100%",
 | 
			
		||||
                      "@md": {
 | 
			
		||||
                        flexDirection: "row",
 | 
			
		||||
                        height: "100%",
 | 
			
		||||
@@ -151,15 +168,15 @@ const Navigation = () => {
 | 
			
		||||
                      css={{
 | 
			
		||||
                        borderBottom: "1px solid $colors$mauve5",
 | 
			
		||||
                        width: "100%",
 | 
			
		||||
                        minWidth: "240px",
 | 
			
		||||
                        flexDirection: "column",
 | 
			
		||||
                        p: "$7",
 | 
			
		||||
                        height: "100%",
 | 
			
		||||
                        backgroundColor: "$mauve2",
 | 
			
		||||
                        "@md": {
 | 
			
		||||
                          width: "30%",
 | 
			
		||||
                          maxWidth: "300px",
 | 
			
		||||
                          borderBottom: "0px",
 | 
			
		||||
                          borderRight: "1px solid $colors$mauve6",
 | 
			
		||||
                          borderRight: "1px solid $colors$mauve5",
 | 
			
		||||
                        },
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
@@ -196,9 +213,9 @@ const Navigation = () => {
 | 
			
		||||
                              display: "inline-flex",
 | 
			
		||||
                              alignItems: "center",
 | 
			
		||||
                              gap: "$3",
 | 
			
		||||
                              color: "$purple10",
 | 
			
		||||
                              color: "$purple11",
 | 
			
		||||
                              "&:hover": {
 | 
			
		||||
                                color: "$purple11",
 | 
			
		||||
                                color: "$purple12",
 | 
			
		||||
                              },
 | 
			
		||||
                              "&:focus": {
 | 
			
		||||
                                outline: 0,
 | 
			
		||||
@@ -217,9 +234,9 @@ const Navigation = () => {
 | 
			
		||||
                              display: "inline-flex",
 | 
			
		||||
                              alignItems: "center",
 | 
			
		||||
                              gap: "$3",
 | 
			
		||||
                              color: "$purple10",
 | 
			
		||||
                              color: "$purple11",
 | 
			
		||||
                              "&:hover": {
 | 
			
		||||
                                color: "$purple11",
 | 
			
		||||
                                color: "$purple12",
 | 
			
		||||
                              },
 | 
			
		||||
                              "&:focus": {
 | 
			
		||||
                                outline: 0,
 | 
			
		||||
@@ -228,7 +245,7 @@ const Navigation = () => {
 | 
			
		||||
                            as="a"
 | 
			
		||||
                            rel="noreferrer noopener"
 | 
			
		||||
                            target="_blank"
 | 
			
		||||
                            href="https://xrpl-hooks.readme.io/docs"
 | 
			
		||||
                            href="https://xrpl-hooks.readme.io/v2.0/docs"
 | 
			
		||||
                          >
 | 
			
		||||
                            <ArrowUpRight size="15px" /> Hooks documentation
 | 
			
		||||
                          </Text>
 | 
			
		||||
@@ -237,9 +254,9 @@ const Navigation = () => {
 | 
			
		||||
                              display: "inline-flex",
 | 
			
		||||
                              alignItems: "center",
 | 
			
		||||
                              gap: "$3",
 | 
			
		||||
                              color: "$purple10",
 | 
			
		||||
                              color: "$purple11",
 | 
			
		||||
                              "&:hover": {
 | 
			
		||||
                                color: "$purple11",
 | 
			
		||||
                                color: "$purple12",
 | 
			
		||||
                              },
 | 
			
		||||
                              "&:focus": {
 | 
			
		||||
                                outline: 0,
 | 
			
		||||
@@ -263,57 +280,33 @@ const Navigation = () => {
 | 
			
		||||
                        gridTemplateRows: "max-content",
 | 
			
		||||
                        flex: 1,
 | 
			
		||||
                        p: "$7",
 | 
			
		||||
                        pb: "$16",
 | 
			
		||||
                        gap: "$3",
 | 
			
		||||
                        alignItems: "flex-start",
 | 
			
		||||
                        alignItems: "normal",
 | 
			
		||||
                        flexWrap: "wrap",
 | 
			
		||||
                        backgroundColor: "$mauve1",
 | 
			
		||||
                        "@md": {
 | 
			
		||||
                          gridTemplateColumns: "1fr 1fr",
 | 
			
		||||
                          gridTemplateRows: "max-content",
 | 
			
		||||
                        },
 | 
			
		||||
                        "@lg": {
 | 
			
		||||
                          gridTemplateColumns: "1fr 1fr 1fr",
 | 
			
		||||
                          gridTemplateRows: "max-content",
 | 
			
		||||
                        },
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      <PanelBox
 | 
			
		||||
                        as="a"
 | 
			
		||||
                        href={`/develop/${templateFileIds.starter}`}
 | 
			
		||||
                      >
 | 
			
		||||
                        <Heading>Starter</Heading>
 | 
			
		||||
                        <Text>
 | 
			
		||||
                          Just a basic starter with essential imports
 | 
			
		||||
                        </Text>
 | 
			
		||||
                      </PanelBox>
 | 
			
		||||
                      <PanelBox
 | 
			
		||||
                        as="a"
 | 
			
		||||
                        href={`/develop/${templateFileIds.firewall}`}
 | 
			
		||||
                      >
 | 
			
		||||
                        <Heading>Firewall</Heading>
 | 
			
		||||
                        <Text>
 | 
			
		||||
                          This Hook essentially checks a blacklist of accounts
 | 
			
		||||
                        </Text>
 | 
			
		||||
                      </PanelBox>
 | 
			
		||||
                      <PanelBox
 | 
			
		||||
                        as="a"
 | 
			
		||||
                        href={`/develop/${templateFileIds.notary}`}
 | 
			
		||||
                      >
 | 
			
		||||
                        <Heading>Notary</Heading>
 | 
			
		||||
                        <Text>
 | 
			
		||||
                          Collecting signatures for multi-sign transactions
 | 
			
		||||
                        </Text>
 | 
			
		||||
                      </PanelBox>
 | 
			
		||||
                      <PanelBox
 | 
			
		||||
                        as="a"
 | 
			
		||||
                        href={`/develop/${templateFileIds.carbon}`}
 | 
			
		||||
                      >
 | 
			
		||||
                        <Heading>Carbon</Heading>
 | 
			
		||||
                        <Text>Send a percentage of sum to an address</Text>
 | 
			
		||||
                      </PanelBox>
 | 
			
		||||
                      <PanelBox
 | 
			
		||||
                        as="a"
 | 
			
		||||
                        href={`/develop/${templateFileIds.peggy}`}
 | 
			
		||||
                      >
 | 
			
		||||
                        <Heading>Peggy</Heading>
 | 
			
		||||
                        <Text>An oracle based stable coin hook</Text>
 | 
			
		||||
                      </PanelBox>
 | 
			
		||||
                      {Object.values(templateFileIds).map((template) => (
 | 
			
		||||
                        <PanelBox
 | 
			
		||||
                          key={template.id}
 | 
			
		||||
                          as="a"
 | 
			
		||||
                          href={`/develop/${template.id}`}
 | 
			
		||||
                        >
 | 
			
		||||
                          <ImageWrapper>{template.icon()}</ImageWrapper>
 | 
			
		||||
                          <Heading>{template.name}</Heading>
 | 
			
		||||
 | 
			
		||||
                          <Text>{template.description}</Text>
 | 
			
		||||
                        </PanelBox>
 | 
			
		||||
                      ))}
 | 
			
		||||
                    </Flex>
 | 
			
		||||
                  </Flex>
 | 
			
		||||
                  <DialogClose asChild>
 | 
			
		||||
@@ -347,6 +340,8 @@ const Navigation = () => {
 | 
			
		||||
              height: 0,
 | 
			
		||||
              background: "transparent",
 | 
			
		||||
            },
 | 
			
		||||
            scrollbarColor: "transparent",
 | 
			
		||||
            scrollbarWidth: "none",
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Stack
 | 
			
		||||
@@ -399,7 +394,7 @@ const Navigation = () => {
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Link>
 | 
			
		||||
            </ButtonGroup>
 | 
			
		||||
            <Link href="https://xrpl-hooks.readme.io/" passHref>
 | 
			
		||||
            <Link href="https://xrpl-hooks.readme.io/v2.0" passHref>
 | 
			
		||||
              <a target="_blank" rel="noreferrer noopener">
 | 
			
		||||
                <Button outline>
 | 
			
		||||
                  <BookOpen size="15px" />
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										109
									
								
								components/Popover.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								components/Popover.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,109 @@
 | 
			
		||||
import React, { ReactNode } from "react";
 | 
			
		||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
 | 
			
		||||
import { styled, keyframes } from "../stitches.config";
 | 
			
		||||
 | 
			
		||||
const slideUpAndFade = keyframes({
 | 
			
		||||
  "0%": { opacity: 0, transform: "translateY(2px)" },
 | 
			
		||||
  "100%": { opacity: 1, transform: "translateY(0)" },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const slideRightAndFade = keyframes({
 | 
			
		||||
  "0%": { opacity: 0, transform: "translateX(-2px)" },
 | 
			
		||||
  "100%": { opacity: 1, transform: "translateX(0)" },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const slideDownAndFade = keyframes({
 | 
			
		||||
  "0%": { opacity: 0, transform: "translateY(-2px)" },
 | 
			
		||||
  "100%": { opacity: 1, transform: "translateY(0)" },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const slideLeftAndFade = keyframes({
 | 
			
		||||
  "0%": { opacity: 0, transform: "translateX(2px)" },
 | 
			
		||||
  "100%": { opacity: 1, transform: "translateX(0)" },
 | 
			
		||||
});
 | 
			
		||||
const StyledContent = styled(PopoverPrimitive.Content, {
 | 
			
		||||
  borderRadius: 4,
 | 
			
		||||
  padding: "$3 $3",
 | 
			
		||||
  fontSize: 12,
 | 
			
		||||
  lineHeight: 1,
 | 
			
		||||
  color: "$text",
 | 
			
		||||
  backgroundColor: "$background",
 | 
			
		||||
  boxShadow:
 | 
			
		||||
    "0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)",
 | 
			
		||||
  "@media (prefers-reduced-motion: no-preference)": {
 | 
			
		||||
    animationDuration: "400ms",
 | 
			
		||||
    animationTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
 | 
			
		||||
    willChange: "transform, opacity",
 | 
			
		||||
    '&[data-state="open"]': {
 | 
			
		||||
      '&[data-side="top"]': { animationName: slideDownAndFade },
 | 
			
		||||
      '&[data-side="right"]': { animationName: slideLeftAndFade },
 | 
			
		||||
      '&[data-side="bottom"]': { animationName: slideUpAndFade },
 | 
			
		||||
      '&[data-side="left"]': { animationName: slideRightAndFade },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  ".dark &": {
 | 
			
		||||
    backgroundColor: "$mauve5",
 | 
			
		||||
    boxShadow:
 | 
			
		||||
      "0px 5px 38px -2px rgba(22, 23, 24, 1), 0px 10px 20px 0px rgba(22, 23, 24, 1)",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const StyledArrow = styled(PopoverPrimitive.Arrow, {
 | 
			
		||||
  fill: "$colors$mauve2",
 | 
			
		||||
  ".dark &": {
 | 
			
		||||
    fill: "$mauve5",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const StyledClose = styled(PopoverPrimitive.Close, {
 | 
			
		||||
  all: "unset",
 | 
			
		||||
  fontFamily: "inherit",
 | 
			
		||||
  borderRadius: "100%",
 | 
			
		||||
  height: 25,
 | 
			
		||||
  width: 25,
 | 
			
		||||
  display: "inline-flex",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  justifyContent: "center",
 | 
			
		||||
  color: "$text",
 | 
			
		||||
  position: "absolute",
 | 
			
		||||
  top: 5,
 | 
			
		||||
  right: 5,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Exports
 | 
			
		||||
export const PopoverRoot = PopoverPrimitive.Root;
 | 
			
		||||
export const PopoverTrigger = PopoverPrimitive.Trigger;
 | 
			
		||||
export const PopoverContent = StyledContent;
 | 
			
		||||
export const PopoverArrow = StyledArrow;
 | 
			
		||||
export const PopoverClose = StyledClose;
 | 
			
		||||
 | 
			
		||||
interface IPopover {
 | 
			
		||||
  content: string | ReactNode;
 | 
			
		||||
  open?: boolean;
 | 
			
		||||
  defaultOpen?: boolean;
 | 
			
		||||
  onOpenChange?: (open: boolean) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Popover: React.FC<
 | 
			
		||||
  IPopover & React.ComponentProps<typeof PopoverContent>
 | 
			
		||||
> = ({
 | 
			
		||||
  children,
 | 
			
		||||
  content,
 | 
			
		||||
  open,
 | 
			
		||||
  defaultOpen = false,
 | 
			
		||||
  onOpenChange,
 | 
			
		||||
  ...rest
 | 
			
		||||
}) => (
 | 
			
		||||
  <PopoverRoot
 | 
			
		||||
    open={open}
 | 
			
		||||
    defaultOpen={defaultOpen}
 | 
			
		||||
    onOpenChange={onOpenChange}
 | 
			
		||||
  >
 | 
			
		||||
    <PopoverTrigger asChild>{children}</PopoverTrigger>
 | 
			
		||||
    <PopoverContent sideOffset={5} {...rest}>
 | 
			
		||||
      {content} <PopoverArrow offset={5} className="arrow" />
 | 
			
		||||
    </PopoverContent>
 | 
			
		||||
  </PopoverRoot>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default Popover;
 | 
			
		||||
							
								
								
									
										337
									
								
								components/RunScript/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										337
									
								
								components/RunScript/index.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,337 @@
 | 
			
		||||
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 = (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);
 | 
			
		||||
 | 
			
		||||
  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 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 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(() => {
 | 
			
		||||
    try {
 | 
			
		||||
      let data: any = {};
 | 
			
		||||
      Object.keys(fields).forEach(key => {
 | 
			
		||||
        data[key] = fields[key].value;
 | 
			
		||||
      });
 | 
			
		||||
      const template = generateHtmlTemplate(content, data);
 | 
			
		||||
 | 
			
		||||
      setIframeCode(template);
 | 
			
		||||
 | 
			
		||||
      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" },
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
  }, [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(() => {
 | 
			
		||||
    if (isDisabled)
 | 
			
		||||
      return toast.error("Please fill in all the required fields.");
 | 
			
		||||
 | 
			
		||||
    state.scriptLogs = [];
 | 
			
		||||
    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}
 | 
			
		||||
                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;
 | 
			
		||||
@@ -1,56 +1,127 @@
 | 
			
		||||
import { FC } from "react";
 | 
			
		||||
import { mauve, mauveDark } from "@radix-ui/colors";
 | 
			
		||||
import { forwardRef } from "react";
 | 
			
		||||
import { mauve, mauveDark, purple, purpleDark } from "@radix-ui/colors";
 | 
			
		||||
import { useTheme } from "next-themes";
 | 
			
		||||
import { styled } from '../stitches.config';
 | 
			
		||||
import dynamic from 'next/dynamic';
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
import dynamic from "next/dynamic";
 | 
			
		||||
import type { Props } from "react-select";
 | 
			
		||||
const SelectInput = dynamic(() => import("react-select"), { ssr: false });
 | 
			
		||||
 | 
			
		||||
const Select: FC<Props> = props => {
 | 
			
		||||
// eslint-disable-next-line react/display-name
 | 
			
		||||
const Select = forwardRef<any, Props>((props, ref) => {
 | 
			
		||||
  const { theme } = useTheme();
 | 
			
		||||
  const isDark = theme === "dark";
 | 
			
		||||
  const colors: any = {
 | 
			
		||||
    // primary: pink.pink9,
 | 
			
		||||
    active: isDark ? purpleDark.purple9 : purple.purple9,
 | 
			
		||||
    activeLight: isDark ? purpleDark.purple5 : purple.purple5,
 | 
			
		||||
    primary: isDark ? mauveDark.mauve4 : mauve.mauve4,
 | 
			
		||||
    secondary: isDark ? mauveDark.mauve8 : mauve.mauve8,
 | 
			
		||||
    background: isDark ? "rgb(10, 10, 10)" : "rgb(245, 245, 245)",
 | 
			
		||||
    background: isDark ? mauveDark.mauve4 : mauve.mauve4,
 | 
			
		||||
    searchText: isDark ? mauveDark.mauve12 : mauve.mauve12,
 | 
			
		||||
    bg: isDark ? mauveDark.mauve1 : mauve.mauve1,
 | 
			
		||||
    dropDownBg: isDark ? mauveDark.mauve5 : mauve.mauve2,
 | 
			
		||||
    mauve4: isDark ? mauveDark.mauve4 : mauve.mauve4,
 | 
			
		||||
    mauve5: isDark ? mauveDark.mauve5 : mauve.mauve5,
 | 
			
		||||
    mauve8: isDark ? mauveDark.mauve8 : mauve.mauve8,
 | 
			
		||||
    mauve9: isDark ? mauveDark.mauve9 : mauve.mauve9,
 | 
			
		||||
    mauve12: isDark ? mauveDark.mauve12 : mauve.mauve12,
 | 
			
		||||
    border: isDark ? mauveDark.mauve10 : mauve.mauve10,
 | 
			
		||||
    placeholder: isDark ? mauveDark.mauve11 : mauve.mauve11,
 | 
			
		||||
  };
 | 
			
		||||
  colors.outline = colors.background;
 | 
			
		||||
  colors.selected = colors.secondary;
 | 
			
		||||
  return (
 | 
			
		||||
    <SelectInput
 | 
			
		||||
      menuPosition="fixed"
 | 
			
		||||
      theme={theme => ({
 | 
			
		||||
        ...theme,
 | 
			
		||||
        spacing: {
 | 
			
		||||
          ...theme.spacing,
 | 
			
		||||
          controlHeight: 30
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      menuPosition={props.menuPosition || "fixed"}
 | 
			
		||||
      styles={{
 | 
			
		||||
        container: (provided) => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...provided,
 | 
			
		||||
            position: "relative",
 | 
			
		||||
          };
 | 
			
		||||
        },
 | 
			
		||||
        colors: {
 | 
			
		||||
          primary: colors.selected,
 | 
			
		||||
          primary25: colors.primary,
 | 
			
		||||
          primary50: colors.primary,
 | 
			
		||||
          primary75: colors.primary,
 | 
			
		||||
          danger: colors.primary,
 | 
			
		||||
          dangerLight: colors.primary,
 | 
			
		||||
          neutral0: colors.background,
 | 
			
		||||
          neutral5: colors.primary,
 | 
			
		||||
          neutral10: colors.primary,
 | 
			
		||||
          neutral20: colors.outline,
 | 
			
		||||
          neutral30: colors.primary,
 | 
			
		||||
          neutral40: colors.primary,
 | 
			
		||||
          neutral50: colors.placeholder,
 | 
			
		||||
          neutral60: colors.primary,
 | 
			
		||||
          neutral70: colors.primary,
 | 
			
		||||
          neutral80: colors.searchText,
 | 
			
		||||
          neutral90: colors.primary,
 | 
			
		||||
        singleValue: (provided) => ({
 | 
			
		||||
          ...provided,
 | 
			
		||||
          color: colors.mauve12,
 | 
			
		||||
        }),
 | 
			
		||||
        menu: (provided) => ({
 | 
			
		||||
          ...provided,
 | 
			
		||||
          backgroundColor: colors.dropDownBg,
 | 
			
		||||
        }),
 | 
			
		||||
        control: (provided, state) => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...provided,
 | 
			
		||||
            minHeight: 0,
 | 
			
		||||
            border: "0px",
 | 
			
		||||
            backgroundColor: colors.mauve4,
 | 
			
		||||
            boxShadow: `0 0 0 1px ${
 | 
			
		||||
              state.isFocused ? colors.border : colors.secondary
 | 
			
		||||
            }`,
 | 
			
		||||
          };
 | 
			
		||||
        },
 | 
			
		||||
      })}
 | 
			
		||||
        input: (provided) => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...provided,
 | 
			
		||||
            color: "$text",
 | 
			
		||||
          };
 | 
			
		||||
        },
 | 
			
		||||
        multiValue: (provided) => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...provided,
 | 
			
		||||
            backgroundColor: colors.mauve8,
 | 
			
		||||
          };
 | 
			
		||||
        },
 | 
			
		||||
        multiValueLabel: (provided) => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...provided,
 | 
			
		||||
            color: colors.mauve12,
 | 
			
		||||
          };
 | 
			
		||||
        },
 | 
			
		||||
        multiValueRemove: (provided) => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...provided,
 | 
			
		||||
            ":hover": {
 | 
			
		||||
              background: colors.mauve9,
 | 
			
		||||
            },
 | 
			
		||||
          };
 | 
			
		||||
        },
 | 
			
		||||
        option: (provided, state) => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...provided,
 | 
			
		||||
            color: colors.searchText,
 | 
			
		||||
            backgroundColor:
 | 
			
		||||
              state.isFocused
 | 
			
		||||
                ? colors.activeLight
 | 
			
		||||
                : colors.dropDownBg,
 | 
			
		||||
            ":hover": {
 | 
			
		||||
              backgroundColor: colors.active,
 | 
			
		||||
              color: "#ffffff",
 | 
			
		||||
            },
 | 
			
		||||
            ":selected": {
 | 
			
		||||
              backgroundColor: "red",
 | 
			
		||||
            },
 | 
			
		||||
          };
 | 
			
		||||
        },
 | 
			
		||||
        indicatorSeparator: (provided) => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...provided,
 | 
			
		||||
            backgroundColor: colors.secondary,
 | 
			
		||||
          };
 | 
			
		||||
        },
 | 
			
		||||
        dropdownIndicator: (provided, state) => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...provided,
 | 
			
		||||
            color: state.isFocused ? colors.border : colors.secondary,
 | 
			
		||||
            ":hover": {
 | 
			
		||||
              color: colors.border,
 | 
			
		||||
            },
 | 
			
		||||
          };
 | 
			
		||||
        },
 | 
			
		||||
      }}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default styled(Select, {});
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										463
									
								
								components/SetHookDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										463
									
								
								components/SetHookDialog.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,463 @@
 | 
			
		||||
import React, { useCallback, useEffect, useState } from "react";
 | 
			
		||||
import { Plus, Trash, X } from "phosphor-react";
 | 
			
		||||
import { Button, Box, Text } from ".";
 | 
			
		||||
import { Stack, Flex, Select } from ".";
 | 
			
		||||
import {
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
  DialogDescription,
 | 
			
		||||
  DialogClose,
 | 
			
		||||
  DialogTrigger,
 | 
			
		||||
} from "./Dialog";
 | 
			
		||||
import { Input, Label } from "./Input";
 | 
			
		||||
import {
 | 
			
		||||
  Controller,
 | 
			
		||||
  SubmitHandler,
 | 
			
		||||
  useFieldArray,
 | 
			
		||||
  useForm,
 | 
			
		||||
} from "react-hook-form";
 | 
			
		||||
 | 
			
		||||
import { deployHook } from "../state/actions";
 | 
			
		||||
import { useSnapshot } from "valtio";
 | 
			
		||||
import state, { IFile, SelectOption } from "../state";
 | 
			
		||||
import toast from "react-hot-toast";
 | 
			
		||||
import { prepareDeployHookTx, sha256 } from "../state/actions/deployHook";
 | 
			
		||||
import estimateFee from "../utils/estimateFee";
 | 
			
		||||
import {
 | 
			
		||||
  getParameters,
 | 
			
		||||
  getInvokeOptions,
 | 
			
		||||
  transactionOptions,
 | 
			
		||||
  SetHookData,
 | 
			
		||||
} from "../utils/setHook";
 | 
			
		||||
import { capitalize } from "../utils/helpers";
 | 
			
		||||
 | 
			
		||||
export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
 | 
			
		||||
  ({ accountAddress }) => {
 | 
			
		||||
    const snap = useSnapshot(state);
 | 
			
		||||
 | 
			
		||||
    const [estimateLoading, setEstimateLoading] = useState(false);
 | 
			
		||||
    const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
 | 
			
		||||
 | 
			
		||||
    const compiledFiles = snap.files.filter(file => file.compiledContent);
 | 
			
		||||
    const activeFile = compiledFiles[snap.activeWat] as IFile | undefined;
 | 
			
		||||
 | 
			
		||||
    const accountOptions: SelectOption[] = snap.accounts.map(acc => ({
 | 
			
		||||
      label: acc.name,
 | 
			
		||||
      value: acc.address,
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    const [selectedAccount, setSelectedAccount] = useState(
 | 
			
		||||
      accountOptions.find(acc => acc.value === accountAddress)
 | 
			
		||||
    );
 | 
			
		||||
    const account = snap.accounts.find(
 | 
			
		||||
      acc => acc.address === selectedAccount?.value
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const getHookNamespace = useCallback(
 | 
			
		||||
      () =>
 | 
			
		||||
        (activeFile && snap.deployValues[activeFile.name]?.HookNamespace) ||
 | 
			
		||||
        activeFile?.name.split(".")[0] ||
 | 
			
		||||
        "",
 | 
			
		||||
      [activeFile, snap.deployValues]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const getDefaultValues = useCallback((): Partial<SetHookData> => {
 | 
			
		||||
      const content = activeFile?.compiledValueSnapshot;
 | 
			
		||||
      return (
 | 
			
		||||
        (activeFile && snap.deployValues[activeFile.name]) || {
 | 
			
		||||
          HookNamespace: getHookNamespace(),
 | 
			
		||||
          Invoke: getInvokeOptions(content),
 | 
			
		||||
          HookParameters: getParameters(content),
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
    }, [activeFile, getHookNamespace, snap.deployValues]);
 | 
			
		||||
 | 
			
		||||
    const {
 | 
			
		||||
      register,
 | 
			
		||||
      handleSubmit,
 | 
			
		||||
      control,
 | 
			
		||||
      watch,
 | 
			
		||||
      setValue,
 | 
			
		||||
      getValues,
 | 
			
		||||
      reset,
 | 
			
		||||
      formState: { errors },
 | 
			
		||||
    } = useForm<SetHookData>({
 | 
			
		||||
      defaultValues: getDefaultValues(),
 | 
			
		||||
    });
 | 
			
		||||
    const { fields, append, remove } = useFieldArray({
 | 
			
		||||
      control,
 | 
			
		||||
      name: "HookParameters", // unique name for your Field Array
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const watchedFee = watch("Fee");
 | 
			
		||||
 | 
			
		||||
    // Reset form if activeFile changes
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
      if (!activeFile) return;
 | 
			
		||||
      const defaultValues = getDefaultValues();
 | 
			
		||||
 | 
			
		||||
      reset(defaultValues);
 | 
			
		||||
    }, [activeFile, getDefaultValues, reset]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
      if (
 | 
			
		||||
        watchedFee &&
 | 
			
		||||
        (watchedFee.includes(".") || watchedFee.includes(","))
 | 
			
		||||
      ) {
 | 
			
		||||
        setValue("Fee", watchedFee.replaceAll(".", "").replaceAll(",", ""));
 | 
			
		||||
      }
 | 
			
		||||
    }, [watchedFee, setValue]);
 | 
			
		||||
    // const {
 | 
			
		||||
    //   fields: grantFields,
 | 
			
		||||
    //   append: grantAppend,
 | 
			
		||||
    //   remove: grantRemove,
 | 
			
		||||
    // } = useFieldArray({
 | 
			
		||||
    //   control,
 | 
			
		||||
    //   name: "HookGrants", // unique name for your Field Array
 | 
			
		||||
    // });
 | 
			
		||||
    const [hashedNamespace, setHashedNamespace] = useState("");
 | 
			
		||||
 | 
			
		||||
    const namespace = watch("HookNamespace", getHookNamespace());
 | 
			
		||||
 | 
			
		||||
    const calculateHashedValue = useCallback(async () => {
 | 
			
		||||
      const hashedVal = await sha256(namespace);
 | 
			
		||||
      setHashedNamespace(hashedVal.toUpperCase());
 | 
			
		||||
    }, [namespace]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
      calculateHashedValue();
 | 
			
		||||
    }, [namespace, calculateHashedValue]);
 | 
			
		||||
 | 
			
		||||
    const calculateFee = useCallback(async () => {
 | 
			
		||||
      if (!account) return;
 | 
			
		||||
 | 
			
		||||
      const formValues = getValues();
 | 
			
		||||
      const tx = await prepareDeployHookTx(account, formValues);
 | 
			
		||||
      if (!tx) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const res = await estimateFee(tx, account);
 | 
			
		||||
      if (res && res.base_fee) {
 | 
			
		||||
        setValue("Fee", Math.round(Number(res.base_fee || "")).toString());
 | 
			
		||||
      }
 | 
			
		||||
    }, [account, getValues, setValue]);
 | 
			
		||||
 | 
			
		||||
    const tooLargeFile = () => {
 | 
			
		||||
      return Boolean(
 | 
			
		||||
        activeFile?.compiledContent?.byteLength &&
 | 
			
		||||
          activeFile?.compiledContent?.byteLength >= 64000
 | 
			
		||||
      );
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onSubmit: SubmitHandler<SetHookData> = async data => {
 | 
			
		||||
      const currAccount = state.accounts.find(
 | 
			
		||||
        acc => acc.address === account?.address
 | 
			
		||||
      );
 | 
			
		||||
      if (!account) return;
 | 
			
		||||
      if (currAccount) currAccount.isLoading = true;
 | 
			
		||||
 | 
			
		||||
      data.HookParameters.forEach(param => {
 | 
			
		||||
        delete param.$metaData;
 | 
			
		||||
        return param;
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const res = await deployHook(account, data);
 | 
			
		||||
      if (currAccount) currAccount.isLoading = false;
 | 
			
		||||
 | 
			
		||||
      if (res && res.engine_result === "tesSUCCESS") {
 | 
			
		||||
        toast.success("Transaction succeeded!");
 | 
			
		||||
        return setIsSetHookDialogOpen(false);
 | 
			
		||||
      }
 | 
			
		||||
      toast.error(`Transaction failed! (${res?.engine_result_message})`);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onOpenChange = useCallback((open: boolean) => {
 | 
			
		||||
      setIsSetHookDialogOpen(open);
 | 
			
		||||
 | 
			
		||||
      if (open) calculateFee();
 | 
			
		||||
    }, [calculateFee]);
 | 
			
		||||
    return (
 | 
			
		||||
      <Dialog open={isSetHookDialogOpen} onOpenChange={onOpenChange}>
 | 
			
		||||
        <DialogTrigger asChild>
 | 
			
		||||
          <Button
 | 
			
		||||
            ghost
 | 
			
		||||
            size="xs"
 | 
			
		||||
            uppercase
 | 
			
		||||
            variant={"secondary"}
 | 
			
		||||
            disabled={
 | 
			
		||||
              !account || account.isLoading || !activeFile || tooLargeFile()
 | 
			
		||||
            }
 | 
			
		||||
          >
 | 
			
		||||
            Set Hook
 | 
			
		||||
          </Button>
 | 
			
		||||
        </DialogTrigger>
 | 
			
		||||
        <DialogContent>
 | 
			
		||||
          <form onSubmit={handleSubmit(onSubmit)}>
 | 
			
		||||
            <DialogTitle>Deploy configuration</DialogTitle>
 | 
			
		||||
            <DialogDescription as="div">
 | 
			
		||||
              <Stack css={{ width: "100%", flex: 1 }}>
 | 
			
		||||
                <Box css={{ width: "100%" }}>
 | 
			
		||||
                  <Label>Account</Label>
 | 
			
		||||
                  <Select
 | 
			
		||||
                    instanceId="deploy-account"
 | 
			
		||||
                    placeholder="Select account"
 | 
			
		||||
                    options={accountOptions}
 | 
			
		||||
                    value={selectedAccount}
 | 
			
		||||
                    onChange={(acc: any) => setSelectedAccount(acc)}
 | 
			
		||||
                  />
 | 
			
		||||
                </Box>
 | 
			
		||||
                <Box css={{ width: "100%" }}>
 | 
			
		||||
                  <Label>Invoke on transactions</Label>
 | 
			
		||||
                  <Controller
 | 
			
		||||
                    name="Invoke"
 | 
			
		||||
                    control={control}
 | 
			
		||||
                    render={({ field }) => (
 | 
			
		||||
                      <Select
 | 
			
		||||
                        {...field}
 | 
			
		||||
                        closeMenuOnSelect={false}
 | 
			
		||||
                        isMulti
 | 
			
		||||
                        menuPosition="fixed"
 | 
			
		||||
                        options={transactionOptions}
 | 
			
		||||
                      />
 | 
			
		||||
                    )}
 | 
			
		||||
                  />
 | 
			
		||||
                </Box>
 | 
			
		||||
                <Box css={{ width: "100%" }}>
 | 
			
		||||
                  <Label>Hook Namespace Seed</Label>
 | 
			
		||||
                  <Input
 | 
			
		||||
                    {...register("HookNamespace", { required: true })}
 | 
			
		||||
                    autoComplete={"off"}
 | 
			
		||||
                  />
 | 
			
		||||
                  {errors.HookNamespace?.type === "required" && (
 | 
			
		||||
                    <Box css={{ display: "inline", color: "$red11" }}>
 | 
			
		||||
                      Namespace is required
 | 
			
		||||
                    </Box>
 | 
			
		||||
                  )}
 | 
			
		||||
                  <Box css={{ mt: "$3" }}>
 | 
			
		||||
                    <Label>Hook Namespace (sha256)</Label>
 | 
			
		||||
                    <Input readOnly value={hashedNamespace} />
 | 
			
		||||
                  </Box>
 | 
			
		||||
                </Box>
 | 
			
		||||
 | 
			
		||||
                <Box css={{ width: "100%" }}>
 | 
			
		||||
                  <Label style={{ marginBottom: "10px", display: "block" }}>
 | 
			
		||||
                    Hook parameters
 | 
			
		||||
                  </Label>
 | 
			
		||||
                  <Stack>
 | 
			
		||||
                    {fields.map((field, index) => (
 | 
			
		||||
                      <Stack key={field.id}>
 | 
			
		||||
                        <Flex column>
 | 
			
		||||
                          <Flex row>
 | 
			
		||||
                            <Input
 | 
			
		||||
                              // important to include key with field's id
 | 
			
		||||
                              placeholder="Parameter name"
 | 
			
		||||
                              readOnly={field.$metaData?.required}
 | 
			
		||||
                              {...register(
 | 
			
		||||
                                `HookParameters.${index}.HookParameter.HookParameterName`
 | 
			
		||||
                              )}
 | 
			
		||||
                            />
 | 
			
		||||
                            <Input
 | 
			
		||||
                              css={{ mx: "$2" }}
 | 
			
		||||
                              placeholder="Value (hex-quoted)"
 | 
			
		||||
                              {...register(
 | 
			
		||||
                                `HookParameters.${index}.HookParameter.HookParameterValue`,
 | 
			
		||||
                                { required: field.$metaData?.required }
 | 
			
		||||
                              )}
 | 
			
		||||
                            />
 | 
			
		||||
                            <Button
 | 
			
		||||
                              onClick={() => remove(index)}
 | 
			
		||||
                              variant="destroy"
 | 
			
		||||
                            >
 | 
			
		||||
                              <Trash weight="regular" size="16px" />
 | 
			
		||||
                            </Button>
 | 
			
		||||
                          </Flex>
 | 
			
		||||
                          {errors.HookParameters?.[index]?.HookParameter
 | 
			
		||||
                            ?.HookParameterValue?.type === "required" && (
 | 
			
		||||
                            <Text error>This field is required</Text>
 | 
			
		||||
                          )}
 | 
			
		||||
                          <Label css={{ fontSize: "$sm", mt: "$1" }}>
 | 
			
		||||
                            {capitalize(field.$metaData?.description)}
 | 
			
		||||
                          </Label>
 | 
			
		||||
                        </Flex>
 | 
			
		||||
                      </Stack>
 | 
			
		||||
                    ))}
 | 
			
		||||
                    <Button
 | 
			
		||||
                      outline
 | 
			
		||||
                      fullWidth
 | 
			
		||||
                      type="button"
 | 
			
		||||
                      onClick={() =>
 | 
			
		||||
                        append({
 | 
			
		||||
                          HookParameter: {
 | 
			
		||||
                            HookParameterName: "",
 | 
			
		||||
                            HookParameterValue: "",
 | 
			
		||||
                          },
 | 
			
		||||
                        })
 | 
			
		||||
                      }
 | 
			
		||||
                    >
 | 
			
		||||
                      <Plus size="16px" />
 | 
			
		||||
                      Add Hook Parameter
 | 
			
		||||
                    </Button>
 | 
			
		||||
                  </Stack>
 | 
			
		||||
                </Box>
 | 
			
		||||
                <Box css={{ width: "100%", position: "relative" }}>
 | 
			
		||||
                  <Label>Fee</Label>
 | 
			
		||||
                  <Box css={{ display: "flex", alignItems: "center" }}>
 | 
			
		||||
                    <Input
 | 
			
		||||
                      type="number"
 | 
			
		||||
                      {...register("Fee", { required: true })}
 | 
			
		||||
                      autoComplete={"off"}
 | 
			
		||||
                      onKeyPress={e => {
 | 
			
		||||
                        if (e.key === "." || e.key === ",") {
 | 
			
		||||
                          e.preventDefault();
 | 
			
		||||
                        }
 | 
			
		||||
                      }}
 | 
			
		||||
                      step="1"
 | 
			
		||||
                      defaultValue={10000}
 | 
			
		||||
                      css={{
 | 
			
		||||
                        "-moz-appearance": "textfield",
 | 
			
		||||
                        "&::-webkit-outer-spin-button": {
 | 
			
		||||
                          "-webkit-appearance": "none",
 | 
			
		||||
                          margin: 0,
 | 
			
		||||
                        },
 | 
			
		||||
                        "&::-webkit-inner-spin-button ": {
 | 
			
		||||
                          "-webkit-appearance": "none",
 | 
			
		||||
                          margin: 0,
 | 
			
		||||
                        },
 | 
			
		||||
                      }}
 | 
			
		||||
                    />
 | 
			
		||||
                    <Button
 | 
			
		||||
                      size="xs"
 | 
			
		||||
                      variant="primary"
 | 
			
		||||
                      outline
 | 
			
		||||
                      isLoading={estimateLoading}
 | 
			
		||||
                      css={{
 | 
			
		||||
                        position: "absolute",
 | 
			
		||||
                        right: "$2",
 | 
			
		||||
                        fontSize: "$xs",
 | 
			
		||||
                        cursor: "pointer",
 | 
			
		||||
                        alignContent: "center",
 | 
			
		||||
                        display: "flex",
 | 
			
		||||
                      }}
 | 
			
		||||
                      onClick={async e => {
 | 
			
		||||
                        e.preventDefault();
 | 
			
		||||
                        if (!account) return;
 | 
			
		||||
                        setEstimateLoading(true);
 | 
			
		||||
                        const formValues = getValues();
 | 
			
		||||
                        try {
 | 
			
		||||
                          const tx = await prepareDeployHookTx(
 | 
			
		||||
                            account,
 | 
			
		||||
                            formValues
 | 
			
		||||
                          );
 | 
			
		||||
                          if (tx) {
 | 
			
		||||
                            const res = await estimateFee(tx, account);
 | 
			
		||||
 | 
			
		||||
                            if (res && res.base_fee) {
 | 
			
		||||
                              setValue(
 | 
			
		||||
                                "Fee",
 | 
			
		||||
                                Math.round(
 | 
			
		||||
                                  Number(res.base_fee || "")
 | 
			
		||||
                                ).toString()
 | 
			
		||||
                              );
 | 
			
		||||
                            }
 | 
			
		||||
                          }
 | 
			
		||||
                        } catch (err) {}
 | 
			
		||||
 | 
			
		||||
                        setEstimateLoading(false);
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      Suggest
 | 
			
		||||
                    </Button>
 | 
			
		||||
                  </Box>
 | 
			
		||||
                  {errors.Fee?.type === "required" && (
 | 
			
		||||
                    <Box css={{ display: "inline", color: "$red11" }}>
 | 
			
		||||
                      Fee is required
 | 
			
		||||
                    </Box>
 | 
			
		||||
                  )}
 | 
			
		||||
                </Box>
 | 
			
		||||
                {/* <Box css={{ width: "100%" }}>
 | 
			
		||||
                <label style={{ marginBottom: "10px", display: "block" }}>
 | 
			
		||||
                  Hook Grants
 | 
			
		||||
                </label>
 | 
			
		||||
                <Stack>
 | 
			
		||||
                  {grantFields.map((field, index) => (
 | 
			
		||||
                    <Stack key={field.id}>
 | 
			
		||||
                      <Input
 | 
			
		||||
                        // important to include key with field's id
 | 
			
		||||
                        placeholder="Authorize"
 | 
			
		||||
                        {...register(
 | 
			
		||||
                          `HookGrants.${index}.HookGrant.Authorize`,
 | 
			
		||||
                          { minLength: 5 }
 | 
			
		||||
                        )}
 | 
			
		||||
                      />
 | 
			
		||||
                      <Input
 | 
			
		||||
                        placeholder="HookHash"
 | 
			
		||||
                        {...register(`HookGrants.${index}.HookGrant.HookHash`, {
 | 
			
		||||
                          minLength: 64,
 | 
			
		||||
                          maxLength: 64,
 | 
			
		||||
                        })}
 | 
			
		||||
                      />
 | 
			
		||||
                      <Button
 | 
			
		||||
                        onClick={() => grantRemove(index)}
 | 
			
		||||
                        variant="destroy"
 | 
			
		||||
                      >
 | 
			
		||||
                        <Trash weight="regular" size="16px" />
 | 
			
		||||
                      </Button>
 | 
			
		||||
                    </Stack>
 | 
			
		||||
                  ))}
 | 
			
		||||
                  <Button
 | 
			
		||||
                    outline
 | 
			
		||||
                    fullWidth
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    onClick={() =>
 | 
			
		||||
                      grantAppend({
 | 
			
		||||
                        HookGrant: {
 | 
			
		||||
                          Authorize: "",
 | 
			
		||||
                          HookHash: "",
 | 
			
		||||
                        },
 | 
			
		||||
                      })
 | 
			
		||||
                    }
 | 
			
		||||
                  >
 | 
			
		||||
                    <Plus size="16px" />
 | 
			
		||||
                    Add Hook Grant
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Stack>
 | 
			
		||||
              </Box> */}
 | 
			
		||||
              </Stack>
 | 
			
		||||
            </DialogDescription>
 | 
			
		||||
 | 
			
		||||
            <Flex
 | 
			
		||||
              css={{
 | 
			
		||||
                marginTop: 25,
 | 
			
		||||
                justifyContent: "flex-end",
 | 
			
		||||
                gap: "$3",
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <DialogClose asChild>
 | 
			
		||||
                <Button outline>Cancel</Button>
 | 
			
		||||
              </DialogClose>
 | 
			
		||||
              {/* <DialogClose asChild> */}
 | 
			
		||||
              <Button
 | 
			
		||||
                variant="primary"
 | 
			
		||||
                type="submit"
 | 
			
		||||
                isLoading={account?.isLoading}
 | 
			
		||||
              >
 | 
			
		||||
                Set Hook
 | 
			
		||||
              </Button>
 | 
			
		||||
              {/* </DialogClose> */}
 | 
			
		||||
            </Flex>
 | 
			
		||||
            <DialogClose asChild>
 | 
			
		||||
              <Box css={{ position: "absolute", top: "$3", right: "$3" }}>
 | 
			
		||||
                <X size="20px" />
 | 
			
		||||
              </Box>
 | 
			
		||||
            </DialogClose>
 | 
			
		||||
          </form>
 | 
			
		||||
        </DialogContent>
 | 
			
		||||
      </Dialog>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
SetHookDialog.displayName = "SetHookDialog";
 | 
			
		||||
 | 
			
		||||
export default SetHookDialog;
 | 
			
		||||
							
								
								
									
										32
									
								
								components/Switch.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								components/Switch.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
import * as SwitchPrimitive from "@radix-ui/react-switch";
 | 
			
		||||
 | 
			
		||||
const StyledSwitch = styled(SwitchPrimitive.Root, {
 | 
			
		||||
  all: "unset",
 | 
			
		||||
  width: 42,
 | 
			
		||||
  height: 25,
 | 
			
		||||
  backgroundColor: "$mauve9",
 | 
			
		||||
  borderRadius: "9999px",
 | 
			
		||||
  position: "relative",
 | 
			
		||||
  boxShadow: `0 2px 10px $colors$mauve2`,
 | 
			
		||||
  WebkitTapHighlightColor: "rgba(0, 0, 0, 0)",
 | 
			
		||||
  "&:focus": { boxShadow: `0 0 0 2px $colors$mauveA2` },
 | 
			
		||||
  '&[data-state="checked"]': { backgroundColor: "$green11" },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const StyledThumb = styled(SwitchPrimitive.Thumb, {
 | 
			
		||||
  display: "block",
 | 
			
		||||
  width: 21,
 | 
			
		||||
  height: 21,
 | 
			
		||||
  backgroundColor: "white",
 | 
			
		||||
  borderRadius: "9999px",
 | 
			
		||||
  boxShadow: `0 2px 2px $colors$mauveA6`,
 | 
			
		||||
  transition: "transform 100ms",
 | 
			
		||||
  transform: "translateX(2px)",
 | 
			
		||||
  willChange: "transform",
 | 
			
		||||
  '&[data-state="checked"]': { transform: "translateX(19px)" },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Exports
 | 
			
		||||
export const Switch = StyledSwitch;
 | 
			
		||||
export const SwitchThumb = StyledThumb;
 | 
			
		||||
@@ -6,7 +6,7 @@ import React, {
 | 
			
		||||
  useCallback,
 | 
			
		||||
} from "react";
 | 
			
		||||
import type { ReactNode, ReactElement } from "react";
 | 
			
		||||
import { Box, Button, Flex, Input, Stack, Text } from ".";
 | 
			
		||||
import { Box, Button, Flex, Input, Label, Pre, Stack, Text } from ".";
 | 
			
		||||
import {
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogTrigger,
 | 
			
		||||
@@ -17,6 +17,8 @@ import {
 | 
			
		||||
} from "./Dialog";
 | 
			
		||||
import { Plus, X } from "phosphor-react";
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
import { capitalize } from "../utils/helpers";
 | 
			
		||||
import ContextMenu, { ContentMenuOption } from "./ContextMenu";
 | 
			
		||||
 | 
			
		||||
const ErrorText = styled(Text, {
 | 
			
		||||
  color: "$error",
 | 
			
		||||
@@ -24,27 +26,39 @@ const ErrorText = styled(Text, {
 | 
			
		||||
  display: "block",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
type Nullable<T> = T | null | undefined | false;
 | 
			
		||||
 | 
			
		||||
interface TabProps {
 | 
			
		||||
  header?: string;
 | 
			
		||||
  children: ReactNode;
 | 
			
		||||
  header: string;
 | 
			
		||||
  children?: ReactNode;
 | 
			
		||||
  renameDisabled?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO customise strings shown
 | 
			
		||||
// TODO customize messages shown
 | 
			
		||||
interface Props {
 | 
			
		||||
  label?: string;
 | 
			
		||||
  activeIndex?: number;
 | 
			
		||||
  activeHeader?: string;
 | 
			
		||||
  headless?: boolean;
 | 
			
		||||
  children: ReactElement<TabProps>[];
 | 
			
		||||
  keepAllAlive?: boolean;
 | 
			
		||||
  defaultExtension?: string;
 | 
			
		||||
  forceDefaultExtension?: boolean;
 | 
			
		||||
  extensionRequired?: boolean;
 | 
			
		||||
  allowedExtensions?: string[];
 | 
			
		||||
  headerExtraValidation?: {
 | 
			
		||||
    regex: string | RegExp;
 | 
			
		||||
    error: string;
 | 
			
		||||
  };
 | 
			
		||||
  onCreateNewTab?: (name: string) => any;
 | 
			
		||||
  onRenameTab?: (index: number, nwName: string, oldName?: string) => any;
 | 
			
		||||
  onCloseTab?: (index: number, header?: string) => any;
 | 
			
		||||
  onChangeActive?: (index: number, header?: string) => any;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Tab = (props: TabProps) => null;
 | 
			
		||||
 | 
			
		||||
export const Tabs = ({
 | 
			
		||||
  label = "Tab",
 | 
			
		||||
  children,
 | 
			
		||||
  activeIndex,
 | 
			
		||||
  activeHeader,
 | 
			
		||||
@@ -52,15 +66,20 @@ export const Tabs = ({
 | 
			
		||||
  keepAllAlive = false,
 | 
			
		||||
  onCreateNewTab,
 | 
			
		||||
  onCloseTab,
 | 
			
		||||
  onChangeActive,
 | 
			
		||||
  onRenameTab,
 | 
			
		||||
  headerExtraValidation,
 | 
			
		||||
  extensionRequired,
 | 
			
		||||
  defaultExtension = "",
 | 
			
		||||
  forceDefaultExtension,
 | 
			
		||||
  allowedExtensions,
 | 
			
		||||
}: Props) => {
 | 
			
		||||
  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 [renamingTab, setRenamingTab] = useState<number | null>(null);
 | 
			
		||||
  const [tabname, setTabname] = useState("");
 | 
			
		||||
  const [newtabError, setNewtabError] = useState<string | null>(null);
 | 
			
		||||
  const [tabnameError, setTabnameError] = useState<string | null>(null);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (activeIndex) setActive(activeIndex);
 | 
			
		||||
@@ -68,58 +87,142 @@ export const Tabs = ({
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (activeHeader) {
 | 
			
		||||
      const idx = tabs.findIndex((tab) => tab.header === activeHeader);
 | 
			
		||||
      setActive(idx);
 | 
			
		||||
      const idx = tabs.findIndex(tab => tab.header === activeHeader);
 | 
			
		||||
      if (idx !== -1) setActive(idx);
 | 
			
		||||
      else setActive(0);
 | 
			
		||||
    }
 | 
			
		||||
  }, [activeHeader, tabs]);
 | 
			
		||||
 | 
			
		||||
  // when filename changes, reset error
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setNewtabError(null);
 | 
			
		||||
  }, [tabname, setNewtabError]);
 | 
			
		||||
    setTabnameError(null);
 | 
			
		||||
  }, [tabname, setTabnameError]);
 | 
			
		||||
 | 
			
		||||
  const validateTabname = useCallback(
 | 
			
		||||
    (tabname: string): { error: string | null } => {
 | 
			
		||||
      if (tabs.find((tab) => tab.header === tabname)) {
 | 
			
		||||
        return { error: "Name already exists." };
 | 
			
		||||
    (tabname: string): { error?: string; result?: string } => {
 | 
			
		||||
      if (!tabname) {
 | 
			
		||||
        return { error: `Please enter ${label.toLocaleLowerCase()} name.` };
 | 
			
		||||
      }
 | 
			
		||||
      return { error: null };
 | 
			
		||||
      let ext = (tabname.includes(".") && tabname.split(".").pop()) || "";
 | 
			
		||||
 | 
			
		||||
      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 && !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 handleCreateTab = useCallback(() => {
 | 
			
		||||
    // add default extension in case omitted
 | 
			
		||||
    let _tabname = tabname.includes(".") ? tabname : tabname + defaultExtension;
 | 
			
		||||
    if (forceDefaultExtension && !_tabname.endsWith(defaultExtension)) {
 | 
			
		||||
      _tabname = _tabname + defaultExtension;
 | 
			
		||||
    }
 | 
			
		||||
  const handleActiveChange = useCallback(
 | 
			
		||||
    (idx: number, header?: string) => {
 | 
			
		||||
      setActive(idx);
 | 
			
		||||
      onChangeActive?.(idx, header);
 | 
			
		||||
    },
 | 
			
		||||
    [onChangeActive]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
    const chk = validateTabname(_tabname);
 | 
			
		||||
    if (chk.error) {
 | 
			
		||||
      setNewtabError(`Error: ${chk.error}`);
 | 
			
		||||
  const handleRenameTab = useCallback(() => {
 | 
			
		||||
    if (renamingTab === null) return;
 | 
			
		||||
 | 
			
		||||
    const res = validateTabname(tabname);
 | 
			
		||||
    if (res.error) {
 | 
			
		||||
      setTabnameError(`Error: ${res.error}`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { result: nwName = tabname } = res;
 | 
			
		||||
 | 
			
		||||
    setRenamingTab(null);
 | 
			
		||||
    setTabname("");
 | 
			
		||||
 | 
			
		||||
    const oldName = tabs[renamingTab]?.header;
 | 
			
		||||
    onRenameTab?.(renamingTab, nwName, oldName);
 | 
			
		||||
 | 
			
		||||
    handleActiveChange(renamingTab, nwName);
 | 
			
		||||
  }, [
 | 
			
		||||
    handleActiveChange,
 | 
			
		||||
    onRenameTab,
 | 
			
		||||
    renamingTab,
 | 
			
		||||
    tabname,
 | 
			
		||||
    tabs,
 | 
			
		||||
    validateTabname,
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
  const handleCreateTab = useCallback(() => {
 | 
			
		||||
    const res = validateTabname(tabname);
 | 
			
		||||
    if (res.error) {
 | 
			
		||||
      setTabnameError(`Error: ${res.error}`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const { result: _tabname = tabname } = res;
 | 
			
		||||
 | 
			
		||||
    setIsNewtabDialogOpen(false);
 | 
			
		||||
    setTabname("");
 | 
			
		||||
    // switch to new tab?
 | 
			
		||||
    setActive(tabs.length);
 | 
			
		||||
 | 
			
		||||
    onCreateNewTab?.(_tabname);
 | 
			
		||||
  }, [tabname, defaultExtension, validateTabname, onCreateNewTab, tabs.length]);
 | 
			
		||||
 | 
			
		||||
    handleActiveChange(tabs.length, _tabname);
 | 
			
		||||
  }, [
 | 
			
		||||
    validateTabname,
 | 
			
		||||
    tabname,
 | 
			
		||||
    onCreateNewTab,
 | 
			
		||||
    handleActiveChange,
 | 
			
		||||
    tabs.length,
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
  const handleCloseTab = useCallback(
 | 
			
		||||
    (idx: number) => {
 | 
			
		||||
      if (idx <= active && active !== 0) {
 | 
			
		||||
        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 (
 | 
			
		||||
    <>
 | 
			
		||||
      {!headless && (
 | 
			
		||||
@@ -128,52 +231,60 @@ export const Tabs = ({
 | 
			
		||||
            gap: "$3",
 | 
			
		||||
            flex: 1,
 | 
			
		||||
            flexWrap: "nowrap",
 | 
			
		||||
            marginBottom: "-1px",
 | 
			
		||||
            marginBottom: "$2",
 | 
			
		||||
            width: "100%",
 | 
			
		||||
            overflow: "auto",
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {tabs.map((tab, idx) => (
 | 
			
		||||
            <Button
 | 
			
		||||
            <ContextMenu
 | 
			
		||||
              key={tab.header}
 | 
			
		||||
              role="tab"
 | 
			
		||||
              tabIndex={idx}
 | 
			
		||||
              onClick={() => setActive(idx)}
 | 
			
		||||
              onKeyPress={() => setActive(idx)}
 | 
			
		||||
              outline={active !== idx}
 | 
			
		||||
              size="sm"
 | 
			
		||||
              css={{
 | 
			
		||||
                "&:hover": {
 | 
			
		||||
                  span: {
 | 
			
		||||
                    visibility: "visible",
 | 
			
		||||
                  },
 | 
			
		||||
                },
 | 
			
		||||
              }}
 | 
			
		||||
              options={
 | 
			
		||||
                [closeOption(idx), renameOption(idx, tab)].filter(
 | 
			
		||||
                  Boolean
 | 
			
		||||
                ) as ContentMenuOption[]
 | 
			
		||||
              }
 | 
			
		||||
            >
 | 
			
		||||
              {tab.header || idx}
 | 
			
		||||
              {onCloseTab && (
 | 
			
		||||
                <Box
 | 
			
		||||
                  as="span"
 | 
			
		||||
                  css={{
 | 
			
		||||
                    display: "flex",
 | 
			
		||||
                    p: "2px",
 | 
			
		||||
                    borderRadius: "$full",
 | 
			
		||||
                    mr: "-4px",
 | 
			
		||||
                    "&:hover": {
 | 
			
		||||
                      // boxSizing: "0px 0px 1px",
 | 
			
		||||
                      backgroundColor: "$mauve2",
 | 
			
		||||
                      color: "$mauve12",
 | 
			
		||||
              <Button
 | 
			
		||||
                role="tab"
 | 
			
		||||
                tabIndex={idx}
 | 
			
		||||
                onClick={() => handleActiveChange(idx, tab.header)}
 | 
			
		||||
                onKeyPress={() => handleActiveChange(idx, tab.header)}
 | 
			
		||||
                outline={active !== idx}
 | 
			
		||||
                size="sm"
 | 
			
		||||
                css={{
 | 
			
		||||
                  "&:hover": {
 | 
			
		||||
                    span: {
 | 
			
		||||
                      visibility: "visible",
 | 
			
		||||
                    },
 | 
			
		||||
                  }}
 | 
			
		||||
                  onClick={(ev: React.MouseEvent<HTMLElement>) => {
 | 
			
		||||
                    ev.stopPropagation();
 | 
			
		||||
                    handleCloseTab(idx);
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <X size="9px" weight="bold" />
 | 
			
		||||
                </Box>
 | 
			
		||||
              )}
 | 
			
		||||
            </Button>
 | 
			
		||||
                  },
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                {tab.header || idx}
 | 
			
		||||
                {onCloseTab && (
 | 
			
		||||
                  <Box
 | 
			
		||||
                    as="span"
 | 
			
		||||
                    css={{
 | 
			
		||||
                      display: "flex",
 | 
			
		||||
                      p: "2px",
 | 
			
		||||
                      borderRadius: "$full",
 | 
			
		||||
                      mr: "-4px",
 | 
			
		||||
                      "&:hover": {
 | 
			
		||||
                        // boxSizing: "0px 0px 1px",
 | 
			
		||||
                        backgroundColor: "$mauve2",
 | 
			
		||||
                        color: "$mauve12",
 | 
			
		||||
                      },
 | 
			
		||||
                    }}
 | 
			
		||||
                    onClick={(ev: React.MouseEvent<HTMLElement>) => {
 | 
			
		||||
                      ev.stopPropagation();
 | 
			
		||||
                      handleCloseTab(idx);
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <X size="9px" weight="bold" />
 | 
			
		||||
                  </Box>
 | 
			
		||||
                )}
 | 
			
		||||
              </Button>
 | 
			
		||||
            </ContextMenu>
 | 
			
		||||
          ))}
 | 
			
		||||
          {onCreateNewTab && (
 | 
			
		||||
            <Dialog
 | 
			
		||||
@@ -186,23 +297,26 @@ export const Tabs = ({
 | 
			
		||||
                  size="sm"
 | 
			
		||||
                  css={{ alignItems: "center", px: "$2", mr: "$3" }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Plus size="16px" /> {tabs.length === 0 && "Add new tab"}
 | 
			
		||||
                  <Plus size="16px" />{" "}
 | 
			
		||||
                  {tabs.length === 0 && `Add new ${label.toLocaleLowerCase()}`}
 | 
			
		||||
                </Button>
 | 
			
		||||
              </DialogTrigger>
 | 
			
		||||
              <DialogContent>
 | 
			
		||||
                <DialogTitle>Create new tab</DialogTitle>
 | 
			
		||||
                <DialogTitle>
 | 
			
		||||
                  Create new {label.toLocaleLowerCase()}
 | 
			
		||||
                </DialogTitle>
 | 
			
		||||
                <DialogDescription>
 | 
			
		||||
                  <label>Tabname</label>
 | 
			
		||||
                  <Label>{label} name</Label>
 | 
			
		||||
                  <Input
 | 
			
		||||
                    value={tabname}
 | 
			
		||||
                    onChange={(e) => setTabname(e.target.value)}
 | 
			
		||||
                    onKeyPress={(e) => {
 | 
			
		||||
                    onChange={e => setTabname(e.target.value)}
 | 
			
		||||
                    onKeyPress={e => {
 | 
			
		||||
                      if (e.key === "Enter") {
 | 
			
		||||
                        handleCreateTab();
 | 
			
		||||
                      }
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                  <ErrorText>{newtabError}</ErrorText>
 | 
			
		||||
                  <ErrorText>{tabnameError}</ErrorText>
 | 
			
		||||
                </DialogDescription>
 | 
			
		||||
 | 
			
		||||
                <Flex
 | 
			
		||||
@@ -227,31 +341,79 @@ export const Tabs = ({
 | 
			
		||||
              </DialogContent>
 | 
			
		||||
            </Dialog>
 | 
			
		||||
          )}
 | 
			
		||||
          {onRenameTab && (
 | 
			
		||||
            <Dialog
 | 
			
		||||
              open={renamingTab !== null}
 | 
			
		||||
              onOpenChange={() => setRenamingTab(null)}
 | 
			
		||||
            >
 | 
			
		||||
              <DialogContent>
 | 
			
		||||
                <DialogTitle>
 | 
			
		||||
                  Rename <Pre>{tabs[renamingTab || 0]?.header}</Pre>
 | 
			
		||||
                </DialogTitle>
 | 
			
		||||
                <DialogDescription>
 | 
			
		||||
                  <Label>Enter new name</Label>
 | 
			
		||||
                  <Input
 | 
			
		||||
                    value={tabname}
 | 
			
		||||
                    onChange={e => setTabname(e.target.value)}
 | 
			
		||||
                    onKeyPress={e => {
 | 
			
		||||
                      if (e.key === "Enter") {
 | 
			
		||||
                        handleRenameTab();
 | 
			
		||||
                      }
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                  <ErrorText>{tabnameError}</ErrorText>
 | 
			
		||||
                </DialogDescription>
 | 
			
		||||
 | 
			
		||||
                <Flex
 | 
			
		||||
                  css={{
 | 
			
		||||
                    marginTop: 25,
 | 
			
		||||
                    justifyContent: "flex-end",
 | 
			
		||||
                    gap: "$3",
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <DialogClose asChild>
 | 
			
		||||
                    <Button outline>Cancel</Button>
 | 
			
		||||
                  </DialogClose>
 | 
			
		||||
                  <Button variant="primary" onClick={handleRenameTab}>
 | 
			
		||||
                    Confirm
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Flex>
 | 
			
		||||
                <DialogClose asChild>
 | 
			
		||||
                  <Box css={{ position: "absolute", top: "$3", right: "$3" }}>
 | 
			
		||||
                    <X size="20px" />
 | 
			
		||||
                  </Box>
 | 
			
		||||
                </DialogClose>
 | 
			
		||||
              </DialogContent>
 | 
			
		||||
            </Dialog>
 | 
			
		||||
          )}
 | 
			
		||||
        </Stack>
 | 
			
		||||
      )}
 | 
			
		||||
      {keepAllAlive ? (
 | 
			
		||||
        tabs.map((tab, idx) => {
 | 
			
		||||
          // TODO Maybe rule out fragments as children
 | 
			
		||||
          if (!isValidElement(tab.children)) {
 | 
			
		||||
            if (active !== idx) return null;
 | 
			
		||||
            return tab.children;
 | 
			
		||||
          }
 | 
			
		||||
          let key = tab.children.key || tab.header || idx;
 | 
			
		||||
          let { children } = tab;
 | 
			
		||||
          let { style, ...props } = children.props;
 | 
			
		||||
          return (
 | 
			
		||||
            <children.type
 | 
			
		||||
              key={key}
 | 
			
		||||
              {...props}
 | 
			
		||||
              style={{ ...style, display: active !== idx ? "none" : undefined }}
 | 
			
		||||
            />
 | 
			
		||||
          );
 | 
			
		||||
        })
 | 
			
		||||
      ) : (
 | 
			
		||||
        <Fragment key={tabs[active].header || active}>
 | 
			
		||||
          {tabs[active].children}
 | 
			
		||||
        </Fragment>
 | 
			
		||||
      )}
 | 
			
		||||
      {keepAllAlive
 | 
			
		||||
        ? tabs.map((tab, idx) => {
 | 
			
		||||
            // TODO Maybe rule out fragments as children
 | 
			
		||||
            if (!isValidElement(tab.children)) {
 | 
			
		||||
              if (active !== idx) return null;
 | 
			
		||||
              return tab.children;
 | 
			
		||||
            }
 | 
			
		||||
            let key = tab.children.key || tab.header || idx;
 | 
			
		||||
            let { children } = tab;
 | 
			
		||||
            let { style, ...props } = children.props;
 | 
			
		||||
            return (
 | 
			
		||||
              <children.type
 | 
			
		||||
                key={key}
 | 
			
		||||
                {...props}
 | 
			
		||||
                style={{
 | 
			
		||||
                  ...style,
 | 
			
		||||
                  display: active !== idx ? "none" : undefined,
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            );
 | 
			
		||||
          })
 | 
			
		||||
        : tabs[active] && (
 | 
			
		||||
            <Fragment key={tabs[active].header || active}>
 | 
			
		||||
              {tabs[active].children}
 | 
			
		||||
            </Fragment>
 | 
			
		||||
          )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -7,20 +7,35 @@ const Text = styled("span", {
 | 
			
		||||
  variants: {
 | 
			
		||||
    small: {
 | 
			
		||||
      true: {
 | 
			
		||||
        fontSize: '$xs'
 | 
			
		||||
      }
 | 
			
		||||
        fontSize: "$xs",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    muted: {
 | 
			
		||||
      true: {
 | 
			
		||||
        color: '$mauve9'
 | 
			
		||||
      }
 | 
			
		||||
        color: "$mauve9",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    error: {
 | 
			
		||||
      true: {
 | 
			
		||||
        color: "$error",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    warning: {
 | 
			
		||||
      true: {
 | 
			
		||||
        color: "$warning",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    monospace: {
 | 
			
		||||
      true: {
 | 
			
		||||
        fontFamily: '$monospace'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
        fontFamily: "$monospace",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    block: {
 | 
			
		||||
      true: {
 | 
			
		||||
        display: "block",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default Text;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										115
									
								
								components/Textarea.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								components/Textarea.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,115 @@
 | 
			
		||||
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;
 | 
			
		||||
							
								
								
									
										92
									
								
								components/Tooltip.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								components/Tooltip.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { styled, keyframes } from "../stitches.config";
 | 
			
		||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
 | 
			
		||||
 | 
			
		||||
const slideUpAndFade = keyframes({
 | 
			
		||||
  "0%": { opacity: 0, transform: "translateY(2px)" },
 | 
			
		||||
  "100%": { opacity: 1, transform: "translateY(0)" },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const slideRightAndFade = keyframes({
 | 
			
		||||
  "0%": { opacity: 0, transform: "translateX(-2px)" },
 | 
			
		||||
  "100%": { opacity: 1, transform: "translateX(0)" },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const slideDownAndFade = keyframes({
 | 
			
		||||
  "0%": { opacity: 0, transform: "translateY(-2px)" },
 | 
			
		||||
  "100%": { opacity: 1, transform: "translateY(0)" },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const slideLeftAndFade = keyframes({
 | 
			
		||||
  "0%": { opacity: 0, transform: "translateX(2px)" },
 | 
			
		||||
  "100%": { opacity: 1, transform: "translateX(0)" },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const StyledContent = styled(TooltipPrimitive.Content, {
 | 
			
		||||
  borderRadius: 4,
 | 
			
		||||
  padding: "$2 $3",
 | 
			
		||||
  fontSize: 12,
 | 
			
		||||
  lineHeight: 1,
 | 
			
		||||
  color: "$text",
 | 
			
		||||
  backgroundColor: "$background",
 | 
			
		||||
  boxShadow:
 | 
			
		||||
    "hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px",
 | 
			
		||||
  "@media (prefers-reduced-motion: no-preference)": {
 | 
			
		||||
    animationDuration: "400ms",
 | 
			
		||||
    animationTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
 | 
			
		||||
    animationFillMode: "forwards",
 | 
			
		||||
    willChange: "transform, opacity",
 | 
			
		||||
    '&[data-state="delayed-open"]': {
 | 
			
		||||
      '&[data-side="top"]': { animationName: slideDownAndFade },
 | 
			
		||||
      '&[data-side="right"]': { animationName: slideLeftAndFade },
 | 
			
		||||
      '&[data-side="bottom"]': { animationName: slideUpAndFade },
 | 
			
		||||
      '&[data-side="left"]': { animationName: slideRightAndFade },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  ".dark &": {
 | 
			
		||||
    boxShadow:
 | 
			
		||||
      "0px 0px 10px 2px rgba(0,0,0,.45), hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px",
 | 
			
		||||
  },
 | 
			
		||||
  ".light &": {
 | 
			
		||||
    boxShadow:
 | 
			
		||||
      "0px 0px 10px 2px rgba(0,0,0,.25), hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const StyledArrow = styled(TooltipPrimitive.Arrow, {
 | 
			
		||||
  fill: "$background",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
interface ITooltip {
 | 
			
		||||
  content: string;
 | 
			
		||||
  open?: boolean;
 | 
			
		||||
  defaultOpen?: boolean;
 | 
			
		||||
  onOpenChange?: (open: boolean) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Tooltip: React.FC<
 | 
			
		||||
  React.ComponentProps<typeof StyledContent> & ITooltip
 | 
			
		||||
> = ({
 | 
			
		||||
  children,
 | 
			
		||||
  content,
 | 
			
		||||
  open,
 | 
			
		||||
  defaultOpen = false,
 | 
			
		||||
  onOpenChange,
 | 
			
		||||
  ...rest
 | 
			
		||||
}) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <TooltipPrimitive.Root
 | 
			
		||||
      open={open}
 | 
			
		||||
      defaultOpen={defaultOpen}
 | 
			
		||||
      onOpenChange={onOpenChange}
 | 
			
		||||
    >
 | 
			
		||||
      <TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
 | 
			
		||||
      <StyledContent side="bottom" align="center" {...rest}>
 | 
			
		||||
        <div dangerouslySetInnerHTML={{ __html: content }} />
 | 
			
		||||
        <StyledArrow offset={5} width={11} height={5} />
 | 
			
		||||
      </StyledContent>
 | 
			
		||||
    </TooltipPrimitive.Root>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Tooltip;
 | 
			
		||||
							
								
								
									
										252
									
								
								components/Transaction/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								components/Transaction/index.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,252 @@
 | 
			
		||||
import { Play } from "phosphor-react";
 | 
			
		||||
import { FC, useCallback, useEffect } from "react";
 | 
			
		||||
import { useSnapshot } from "valtio";
 | 
			
		||||
import state from "../../state";
 | 
			
		||||
import {
 | 
			
		||||
  defaultTransactionType,
 | 
			
		||||
  getTxFields,
 | 
			
		||||
  modifyTxState,
 | 
			
		||||
  prepareState,
 | 
			
		||||
  prepareTransaction,
 | 
			
		||||
  SelectOption,
 | 
			
		||||
  TransactionState,
 | 
			
		||||
} from "../../state/transactions";
 | 
			
		||||
import { sendTransaction } from "../../state/actions";
 | 
			
		||||
import Box from "../Box";
 | 
			
		||||
import Button from "../Button";
 | 
			
		||||
import Flex from "../Flex";
 | 
			
		||||
import { TxJson } from "./json";
 | 
			
		||||
import { TxUI } from "./ui";
 | 
			
		||||
import { default as _estimateFee } from "../../utils/estimateFee";
 | 
			
		||||
import toast from "react-hot-toast";
 | 
			
		||||
 | 
			
		||||
export interface TransactionProps {
 | 
			
		||||
  header: string;
 | 
			
		||||
  state: TransactionState;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Transaction: FC<TransactionProps> = ({
 | 
			
		||||
  header,
 | 
			
		||||
  state: txState,
 | 
			
		||||
  ...props
 | 
			
		||||
}) => {
 | 
			
		||||
  const { accounts, editorSettings } = useSnapshot(state);
 | 
			
		||||
  const {
 | 
			
		||||
    selectedAccount,
 | 
			
		||||
    selectedTransaction,
 | 
			
		||||
    txIsDisabled,
 | 
			
		||||
    txIsLoading,
 | 
			
		||||
    viewType,
 | 
			
		||||
    editorValue,
 | 
			
		||||
  } = txState;
 | 
			
		||||
 | 
			
		||||
  const setState = useCallback(
 | 
			
		||||
    (pTx?: Partial<TransactionState>) => {
 | 
			
		||||
      return modifyTxState(header, pTx);
 | 
			
		||||
    },
 | 
			
		||||
    [header]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const prepareOptions = useCallback(
 | 
			
		||||
    (state: Partial<TransactionState> = txState) => {
 | 
			
		||||
      const {
 | 
			
		||||
        selectedTransaction,
 | 
			
		||||
        selectedDestAccount,
 | 
			
		||||
        selectedAccount,
 | 
			
		||||
        txFields,
 | 
			
		||||
      } = state;
 | 
			
		||||
 | 
			
		||||
      const TransactionType = selectedTransaction?.value || null;
 | 
			
		||||
      const Destination = selectedDestAccount?.value || txFields?.Destination;
 | 
			
		||||
      const Account = selectedAccount?.value || null;
 | 
			
		||||
 | 
			
		||||
      return prepareTransaction({
 | 
			
		||||
        ...txFields,
 | 
			
		||||
        TransactionType,
 | 
			
		||||
        Destination,
 | 
			
		||||
        Account,
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    [txState]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const transactionType = selectedTransaction?.value;
 | 
			
		||||
    const account = selectedAccount?.value;
 | 
			
		||||
    if (!account || !transactionType || txIsLoading) {
 | 
			
		||||
      setState({ txIsDisabled: true });
 | 
			
		||||
    } else {
 | 
			
		||||
      setState({ txIsDisabled: false });
 | 
			
		||||
    }
 | 
			
		||||
  }, [
 | 
			
		||||
    selectedAccount?.value,
 | 
			
		||||
    selectedTransaction?.value,
 | 
			
		||||
    setState,
 | 
			
		||||
    txIsLoading,
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
  const submitTest = useCallback(async () => {
 | 
			
		||||
    let st: TransactionState | undefined;
 | 
			
		||||
    const tt = txState.selectedTransaction?.value;
 | 
			
		||||
    if (viewType === "json") {
 | 
			
		||||
      // save the editor state first
 | 
			
		||||
      const pst = prepareState(editorValue || "", tt);
 | 
			
		||||
      if (!pst) return;
 | 
			
		||||
 | 
			
		||||
      st = setState(pst);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const account = accounts.find(
 | 
			
		||||
      acc => acc.address === selectedAccount?.value
 | 
			
		||||
    );
 | 
			
		||||
    if (txIsDisabled) return;
 | 
			
		||||
 | 
			
		||||
    setState({ txIsLoading: true });
 | 
			
		||||
    const logPrefix = header ? `${header.split(".")[0]}: ` : undefined;
 | 
			
		||||
    try {
 | 
			
		||||
      if (!account) {
 | 
			
		||||
        throw Error("Account must be selected from imported accounts!");
 | 
			
		||||
      }
 | 
			
		||||
      const options = prepareOptions(st);
 | 
			
		||||
 | 
			
		||||
      const fields = getTxFields(options.TransactionType);
 | 
			
		||||
      if (fields.Destination && !options.Destination) {
 | 
			
		||||
        throw Error("Destination account is required!");
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await sendTransaction(account, options, { logPrefix });
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(error);
 | 
			
		||||
      if (error instanceof Error) {
 | 
			
		||||
        state.transactionLogs.push({
 | 
			
		||||
          type: "error",
 | 
			
		||||
          message: `${logPrefix}${error.message}`,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    setState({ txIsLoading: false });
 | 
			
		||||
  }, [
 | 
			
		||||
    viewType,
 | 
			
		||||
    accounts,
 | 
			
		||||
    txIsDisabled,
 | 
			
		||||
    setState,
 | 
			
		||||
    header,
 | 
			
		||||
    editorValue,
 | 
			
		||||
    txState,
 | 
			
		||||
    selectedAccount?.value,
 | 
			
		||||
    prepareOptions,
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
  const getJsonString = useCallback(
 | 
			
		||||
    (state?: Partial<TransactionState>) =>
 | 
			
		||||
      JSON.stringify(
 | 
			
		||||
        prepareOptions?.(state) || {},
 | 
			
		||||
        null,
 | 
			
		||||
        editorSettings.tabSize
 | 
			
		||||
      ),
 | 
			
		||||
    [editorSettings.tabSize, prepareOptions]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const resetState = useCallback(
 | 
			
		||||
    (transactionType: SelectOption | undefined = defaultTransactionType) => {
 | 
			
		||||
      const fields = getTxFields(transactionType?.value);
 | 
			
		||||
 | 
			
		||||
      const nwState: Partial<TransactionState> = {
 | 
			
		||||
        viewType,
 | 
			
		||||
        selectedTransaction: transactionType,
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      if (fields.Destination !== undefined) {
 | 
			
		||||
        nwState.selectedDestAccount = null;
 | 
			
		||||
        fields.Destination = "";
 | 
			
		||||
      } else {
 | 
			
		||||
        fields.Destination = undefined;
 | 
			
		||||
      }
 | 
			
		||||
      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]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
 | 
			
		||||
      {viewType === "json" ? (
 | 
			
		||||
        <TxJson
 | 
			
		||||
          getJsonString={getJsonString}
 | 
			
		||||
          header={header}
 | 
			
		||||
          state={txState}
 | 
			
		||||
          setState={setState}
 | 
			
		||||
          estimateFee={estimateFee}
 | 
			
		||||
        />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <TxUI state={txState} setState={setState} estimateFee={estimateFee} />
 | 
			
		||||
      )}
 | 
			
		||||
      <Flex
 | 
			
		||||
        row
 | 
			
		||||
        css={{
 | 
			
		||||
          justifyContent: "space-between",
 | 
			
		||||
          position: "absolute",
 | 
			
		||||
          left: 0,
 | 
			
		||||
          bottom: 0,
 | 
			
		||||
          width: "100%",
 | 
			
		||||
          mb: "$1",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Button
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            if (viewType === "ui") {
 | 
			
		||||
              setState({ viewType: "json" });
 | 
			
		||||
            } else setState({ viewType: "ui" });
 | 
			
		||||
          }}
 | 
			
		||||
          outline
 | 
			
		||||
        >
 | 
			
		||||
          {viewType === "ui" ? "EDIT AS JSON" : "EXIT JSON MODE"}
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Flex row>
 | 
			
		||||
          <Button onClick={() => resetState()} outline css={{ mr: "$3" }}>
 | 
			
		||||
            RESET
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button
 | 
			
		||||
            variant="primary"
 | 
			
		||||
            onClick={submitTest}
 | 
			
		||||
            isLoading={txIsLoading}
 | 
			
		||||
            disabled={txIsDisabled}
 | 
			
		||||
          >
 | 
			
		||||
            <Play weight="bold" size="16px" />
 | 
			
		||||
            RUN TEST
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Flex>
 | 
			
		||||
      </Flex>
 | 
			
		||||
    </Box>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Transaction;
 | 
			
		||||
							
								
								
									
										232
									
								
								components/Transaction/json.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								components/Transaction/json.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,232 @@
 | 
			
		||||
import { FC, useCallback, useEffect, useMemo, useState } from "react";
 | 
			
		||||
import { useSnapshot } from "valtio";
 | 
			
		||||
import state, {
 | 
			
		||||
  prepareState,
 | 
			
		||||
  transactionsData,
 | 
			
		||||
  TransactionState,
 | 
			
		||||
} from "../../state";
 | 
			
		||||
import Text from "../Text";
 | 
			
		||||
import { Flex, Link } from "..";
 | 
			
		||||
import { showAlert } from "../../state/actions/showAlert";
 | 
			
		||||
import { parseJSON } from "../../utils/json";
 | 
			
		||||
import { extractSchemaProps } from "../../utils/schema";
 | 
			
		||||
import amountSchema from "../../content/amount-schema.json";
 | 
			
		||||
import Monaco from "../Monaco";
 | 
			
		||||
import type monaco from "monaco-editor";
 | 
			
		||||
 | 
			
		||||
interface JsonProps {
 | 
			
		||||
  getJsonString?: (state?: Partial<TransactionState>) => string;
 | 
			
		||||
  header?: string;
 | 
			
		||||
  setState: (pTx?: Partial<TransactionState> | undefined) => void;
 | 
			
		||||
  state: TransactionState;
 | 
			
		||||
  estimateFee?: () => Promise<string | undefined>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const TxJson: FC<JsonProps> = ({
 | 
			
		||||
  getJsonString,
 | 
			
		||||
  state: txState,
 | 
			
		||||
  header,
 | 
			
		||||
  setState,
 | 
			
		||||
}) => {
 | 
			
		||||
  const { editorSettings, accounts } = useSnapshot(state);
 | 
			
		||||
  const { editorValue, estimatedFee } = txState;
 | 
			
		||||
  const [currTxType, setCurrTxType] = useState<string | undefined>(
 | 
			
		||||
    txState.selectedTransaction?.value
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setState({
 | 
			
		||||
      editorValue: getJsonString?.(),
 | 
			
		||||
    });
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const parsed = parseJSON(editorValue);
 | 
			
		||||
    if (!parsed) return;
 | 
			
		||||
 | 
			
		||||
    const tt = parsed.TransactionType;
 | 
			
		||||
    const tx = transactionsData.find(t => t.TransactionType === tt);
 | 
			
		||||
    if (tx) setCurrTxType(tx.TransactionType);
 | 
			
		||||
    else {
 | 
			
		||||
      setCurrTxType(undefined);
 | 
			
		||||
    }
 | 
			
		||||
  }, [editorValue]);
 | 
			
		||||
 | 
			
		||||
  const saveState = (value: string, transactionType?: string) => {
 | 
			
		||||
    const tx = prepareState(value, transactionType);
 | 
			
		||||
    if (tx) {
 | 
			
		||||
      setState(tx);
 | 
			
		||||
      setState({
 | 
			
		||||
        editorValue: getJsonString?.(tx),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const discardChanges = () => {
 | 
			
		||||
    showAlert("Confirm", {
 | 
			
		||||
      body: "Are you sure to discard these changes?",
 | 
			
		||||
      confirmText: "Yes",
 | 
			
		||||
      onCancel: () => {},
 | 
			
		||||
      onConfirm: () => setState({ editorValue: getJsonString?.() }),
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onExit = (value: string) => {
 | 
			
		||||
    const options = parseJSON(value);
 | 
			
		||||
    if (options) {
 | 
			
		||||
      saveState(value, currTxType);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    showAlert("Error!", {
 | 
			
		||||
      body: `Malformed Transaction in ${header}, would you like to discard these changes?`,
 | 
			
		||||
      confirmText: "Discard",
 | 
			
		||||
      onConfirm: () => setState({ editorValue: getJsonString?.() }),
 | 
			
		||||
      onCancel: () => setState({ viewType: "json" }),
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const getSchemas = useCallback(async (): Promise<any[]> => {
 | 
			
		||||
    const txObj = transactionsData.find(
 | 
			
		||||
      td => td.TransactionType === currTxType
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let genericSchemaProps: any;
 | 
			
		||||
    if (txObj) {
 | 
			
		||||
      genericSchemaProps = extractSchemaProps(txObj);
 | 
			
		||||
    } else {
 | 
			
		||||
      genericSchemaProps = transactionsData.reduce(
 | 
			
		||||
        (cumm, td) => ({
 | 
			
		||||
          ...cumm,
 | 
			
		||||
          ...extractSchemaProps(td),
 | 
			
		||||
        }),
 | 
			
		||||
        {}
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return [
 | 
			
		||||
      {
 | 
			
		||||
        uri: "file:///main-schema.json", // id of the first schema
 | 
			
		||||
        fileMatch: ["**.json"], // associate with our model
 | 
			
		||||
        schema: {
 | 
			
		||||
          title: header,
 | 
			
		||||
          type: "object",
 | 
			
		||||
          required: ["TransactionType", "Account"],
 | 
			
		||||
          properties: {
 | 
			
		||||
            ...genericSchemaProps,
 | 
			
		||||
            TransactionType: {
 | 
			
		||||
              title: "Transaction Type",
 | 
			
		||||
              enum: transactionsData.map(td => td.TransactionType),
 | 
			
		||||
            },
 | 
			
		||||
            Account: {
 | 
			
		||||
              $ref: "file:///account-schema.json",
 | 
			
		||||
            },
 | 
			
		||||
            Destination: {
 | 
			
		||||
              anyOf: [
 | 
			
		||||
                {
 | 
			
		||||
                  $ref: "file:///account-schema.json",
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  type: "string",
 | 
			
		||||
                  title: "Destination Account",
 | 
			
		||||
                },
 | 
			
		||||
              ],
 | 
			
		||||
            },
 | 
			
		||||
            Amount: {
 | 
			
		||||
              $ref: "file:///amount-schema.json",
 | 
			
		||||
            },
 | 
			
		||||
            Fee: {
 | 
			
		||||
              $ref: "file:///fee-schema.json",
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        uri: "file:///account-schema.json",
 | 
			
		||||
        schema: {
 | 
			
		||||
          type: "string",
 | 
			
		||||
          title: "Account type",
 | 
			
		||||
          enum: accounts.map(acc => acc.address),
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        uri: "file:///fee-schema.json",
 | 
			
		||||
        schema: {
 | 
			
		||||
          type: "string",
 | 
			
		||||
          title: "Fee type",
 | 
			
		||||
          const: estimatedFee,
 | 
			
		||||
          description: estimatedFee
 | 
			
		||||
            ? "Above mentioned value is recommended base fee"
 | 
			
		||||
            : undefined,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        ...amountSchema,
 | 
			
		||||
      },
 | 
			
		||||
    ];
 | 
			
		||||
  }, [accounts, currTxType, estimatedFee, header]);
 | 
			
		||||
 | 
			
		||||
  const [monacoInst, setMonacoInst] = useState<typeof monaco>();
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!monacoInst) return;
 | 
			
		||||
    getSchemas().then(schemas => {
 | 
			
		||||
      monacoInst.languages.json.jsonDefaults.setDiagnosticsOptions({
 | 
			
		||||
        validate: true,
 | 
			
		||||
        schemas,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }, [getSchemas, monacoInst]);
 | 
			
		||||
 | 
			
		||||
  const hasUnsaved = useMemo(
 | 
			
		||||
    () => editorValue !== getJsonString?.(),
 | 
			
		||||
    [editorValue, getJsonString]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Monaco
 | 
			
		||||
      rootProps={{
 | 
			
		||||
        css: { height: "calc(100% - 45px)" },
 | 
			
		||||
      }}
 | 
			
		||||
      language={"json"}
 | 
			
		||||
      id={header}
 | 
			
		||||
      height="100%"
 | 
			
		||||
      value={editorValue}
 | 
			
		||||
      onChange={val => setState({ editorValue: val })}
 | 
			
		||||
      onMount={(editor, monaco) => {
 | 
			
		||||
        editor.updateOptions({
 | 
			
		||||
          minimap: { enabled: false },
 | 
			
		||||
          glyphMargin: true,
 | 
			
		||||
          tabSize: editorSettings.tabSize,
 | 
			
		||||
          dragAndDrop: true,
 | 
			
		||||
          fontSize: 14,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        setMonacoInst(monaco);
 | 
			
		||||
        // register onExit cb
 | 
			
		||||
        const model = editor.getModel();
 | 
			
		||||
        model?.onWillDispose(() => onExit(model.getValue()));
 | 
			
		||||
      }}
 | 
			
		||||
      overlay={
 | 
			
		||||
        hasUnsaved ? (
 | 
			
		||||
          <Flex
 | 
			
		||||
            row
 | 
			
		||||
            align="center"
 | 
			
		||||
            css={{ fontSize: "$xs", color: "$textMuted", ml: "auto" }}
 | 
			
		||||
          >
 | 
			
		||||
            <Text muted small>
 | 
			
		||||
              This file has unsaved changes.
 | 
			
		||||
            </Text>
 | 
			
		||||
            <Link
 | 
			
		||||
              css={{ ml: "$1" }}
 | 
			
		||||
              onClick={() => saveState(editorValue || "", currTxType)}
 | 
			
		||||
            >
 | 
			
		||||
              save
 | 
			
		||||
            </Link>
 | 
			
		||||
            <Link css={{ ml: "$1" }} onClick={discardChanges}>
 | 
			
		||||
              discard
 | 
			
		||||
            </Link>
 | 
			
		||||
          </Flex>
 | 
			
		||||
        ) : undefined
 | 
			
		||||
      }
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										331
									
								
								components/Transaction/ui.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										331
									
								
								components/Transaction/ui.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,331 @@
 | 
			
		||||
import { FC, useCallback, useEffect, useMemo, useState } from "react";
 | 
			
		||||
import Container from "../Container";
 | 
			
		||||
import Flex from "../Flex";
 | 
			
		||||
import Input from "../Input";
 | 
			
		||||
import Select from "../Select";
 | 
			
		||||
import Text from "../Text";
 | 
			
		||||
import {
 | 
			
		||||
  SelectOption,
 | 
			
		||||
  TransactionState,
 | 
			
		||||
  transactionsOptions,
 | 
			
		||||
  TxFields,
 | 
			
		||||
  getTxFields,
 | 
			
		||||
  defaultTransactionType,
 | 
			
		||||
} from "../../state/transactions";
 | 
			
		||||
import { useSnapshot } from "valtio";
 | 
			
		||||
import state from "../../state";
 | 
			
		||||
import { streamState } from "../DebugStream";
 | 
			
		||||
import { Button } from "..";
 | 
			
		||||
import Textarea from "../Textarea";
 | 
			
		||||
 | 
			
		||||
interface UIProps {
 | 
			
		||||
  setState: (
 | 
			
		||||
    pTx?: Partial<TransactionState> | undefined
 | 
			
		||||
  ) => TransactionState | undefined;
 | 
			
		||||
  state: TransactionState;
 | 
			
		||||
  estimateFee?: (...arg: any) => Promise<string | undefined>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const TxUI: FC<UIProps> = ({
 | 
			
		||||
  state: txState,
 | 
			
		||||
  setState,
 | 
			
		||||
  estimateFee,
 | 
			
		||||
}) => {
 | 
			
		||||
  const { accounts } = useSnapshot(state);
 | 
			
		||||
  const {
 | 
			
		||||
    selectedAccount,
 | 
			
		||||
    selectedDestAccount,
 | 
			
		||||
    selectedTransaction,
 | 
			
		||||
    txFields,
 | 
			
		||||
  } = txState;
 | 
			
		||||
 | 
			
		||||
  const accountOptions: SelectOption[] = accounts.map(acc => ({
 | 
			
		||||
    label: acc.name,
 | 
			
		||||
    value: acc.address,
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  const destAccountOptions: SelectOption[] = accounts
 | 
			
		||||
    .map(acc => ({
 | 
			
		||||
      label: acc.name,
 | 
			
		||||
      value: acc.address,
 | 
			
		||||
    }))
 | 
			
		||||
    .filter(acc => acc.value !== selectedAccount?.value);
 | 
			
		||||
 | 
			
		||||
  const [feeLoading, setFeeLoading] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const resetFields = useCallback(
 | 
			
		||||
    (tt: string) => {
 | 
			
		||||
      const fields = getTxFields(tt);
 | 
			
		||||
 | 
			
		||||
      if (fields.Destination !== undefined) {
 | 
			
		||||
        setState({ selectedDestAccount: null });
 | 
			
		||||
        fields.Destination = "";
 | 
			
		||||
      } else {
 | 
			
		||||
        fields.Destination = undefined;
 | 
			
		||||
      }
 | 
			
		||||
      return setState({ txFields: fields });
 | 
			
		||||
    },
 | 
			
		||||
    [setState]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const handleSetAccount = (acc: SelectOption) => {
 | 
			
		||||
    setState({ selectedAccount: acc });
 | 
			
		||||
    streamState.selectedAccount = acc;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleSetField = useCallback(
 | 
			
		||||
    (field: keyof TxFields, value: string, opFields?: TxFields) => {
 | 
			
		||||
      const fields = opFields || txFields;
 | 
			
		||||
      const obj = fields[field];
 | 
			
		||||
      setState({
 | 
			
		||||
        txFields: {
 | 
			
		||||
          ...fields,
 | 
			
		||||
          [field]: typeof obj === "object" ? { ...obj, $value: value } : value,
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    [setState, txFields]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const handleEstimateFee = useCallback(
 | 
			
		||||
    async (state?: TransactionState, silent?: boolean) => {
 | 
			
		||||
      setFeeLoading(true);
 | 
			
		||||
 | 
			
		||||
      const fee = await estimateFee?.(state, { silent });
 | 
			
		||||
      if (fee) handleSetField("Fee", fee, state?.txFields);
 | 
			
		||||
 | 
			
		||||
      setFeeLoading(false);
 | 
			
		||||
    },
 | 
			
		||||
    [estimateFee, handleSetField]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const handleChangeTxType = useCallback(
 | 
			
		||||
    (tt: SelectOption) => {
 | 
			
		||||
      setState({ selectedTransaction: tt });
 | 
			
		||||
 | 
			
		||||
      const newState = resetFields(tt.value);
 | 
			
		||||
 | 
			
		||||
      handleEstimateFee(newState, true);
 | 
			
		||||
    },
 | 
			
		||||
    [handleEstimateFee, resetFields, setState]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const switchToJson = () => setState({ viewType: "json" });
 | 
			
		||||
 | 
			
		||||
  // default tx
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (selectedTransaction?.value) return;
 | 
			
		||||
 | 
			
		||||
    if (defaultTransactionType) {
 | 
			
		||||
      handleChangeTxType(defaultTransactionType);
 | 
			
		||||
    }
 | 
			
		||||
  }, [handleChangeTxType, selectedTransaction?.value]);
 | 
			
		||||
 | 
			
		||||
  const fields = useMemo(
 | 
			
		||||
    () => getTxFields(selectedTransaction?.value),
 | 
			
		||||
    [selectedTransaction?.value]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const specialFields = ["TransactionType", "Account"];
 | 
			
		||||
  if (fields.Destination !== undefined) {
 | 
			
		||||
    specialFields.push("Destination");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const otherFields = Object.keys(txFields).filter(
 | 
			
		||||
    k => !specialFields.includes(k)
 | 
			
		||||
  ) as [keyof TxFields];
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Container
 | 
			
		||||
      css={{
 | 
			
		||||
        p: "$3 01",
 | 
			
		||||
        fontSize: "$sm",
 | 
			
		||||
        height: "calc(100% - 45px)",
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Flex column fluid css={{ height: "100%", overflowY: "auto", pr: "$1" }}>
 | 
			
		||||
        <Flex
 | 
			
		||||
          row
 | 
			
		||||
          fluid
 | 
			
		||||
          css={{
 | 
			
		||||
            justifyContent: "flex-end",
 | 
			
		||||
            alignItems: "center",
 | 
			
		||||
            mb: "$3",
 | 
			
		||||
            mt: "1px",
 | 
			
		||||
            pr: "1px",
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Text muted css={{ mr: "$3" }}>
 | 
			
		||||
            Transaction type:{" "}
 | 
			
		||||
          </Text>
 | 
			
		||||
          <Select
 | 
			
		||||
            instanceId="transactionsType"
 | 
			
		||||
            placeholder="Select transaction type"
 | 
			
		||||
            options={transactionsOptions}
 | 
			
		||||
            hideSelectedOptions
 | 
			
		||||
            css={{ width: "70%" }}
 | 
			
		||||
            value={selectedTransaction}
 | 
			
		||||
            onChange={(tt: any) => handleChangeTxType(tt)}
 | 
			
		||||
          />
 | 
			
		||||
        </Flex>
 | 
			
		||||
        <Flex
 | 
			
		||||
          row
 | 
			
		||||
          fluid
 | 
			
		||||
          css={{
 | 
			
		||||
            justifyContent: "flex-end",
 | 
			
		||||
            alignItems: "center",
 | 
			
		||||
            mb: "$3",
 | 
			
		||||
            pr: "1px",
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Text muted css={{ mr: "$3" }}>
 | 
			
		||||
            Account:{" "}
 | 
			
		||||
          </Text>
 | 
			
		||||
          <Select
 | 
			
		||||
            instanceId="from-account"
 | 
			
		||||
            placeholder="Select your account"
 | 
			
		||||
            css={{ width: "70%" }}
 | 
			
		||||
            options={accountOptions}
 | 
			
		||||
            value={selectedAccount}
 | 
			
		||||
            onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc
 | 
			
		||||
          />
 | 
			
		||||
        </Flex>
 | 
			
		||||
        {fields.Destination !== undefined && (
 | 
			
		||||
          <Flex
 | 
			
		||||
            row
 | 
			
		||||
            fluid
 | 
			
		||||
            css={{
 | 
			
		||||
              justifyContent: "flex-end",
 | 
			
		||||
              alignItems: "center",
 | 
			
		||||
              mb: "$3",
 | 
			
		||||
              pr: "1px",
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Text muted css={{ mr: "$3" }}>
 | 
			
		||||
              Destination account:{" "}
 | 
			
		||||
            </Text>
 | 
			
		||||
            <Select
 | 
			
		||||
              instanceId="to-account"
 | 
			
		||||
              placeholder="Select the destination account"
 | 
			
		||||
              css={{ width: "70%" }}
 | 
			
		||||
              options={destAccountOptions}
 | 
			
		||||
              value={selectedDestAccount}
 | 
			
		||||
              isClearable
 | 
			
		||||
              onChange={(acc: any) => setState({ selectedDestAccount: acc })}
 | 
			
		||||
            />
 | 
			
		||||
          </Flex>
 | 
			
		||||
        )}
 | 
			
		||||
        {otherFields.map(field => {
 | 
			
		||||
          let _value = txFields[field];
 | 
			
		||||
 | 
			
		||||
          let value: string | undefined;
 | 
			
		||||
          if (typeof _value === "object") {
 | 
			
		||||
            if (_value.$type === "json" && typeof _value.$value === "object") {
 | 
			
		||||
              value = JSON.stringify(_value.$value, null, 2);
 | 
			
		||||
            } else {
 | 
			
		||||
              value = _value.$value.toString();
 | 
			
		||||
            }
 | 
			
		||||
          } else {
 | 
			
		||||
            value = _value?.toString();
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          const isXrp = typeof _value === "object" && _value.$type === "xrp";
 | 
			
		||||
          const isJson = typeof _value === "object" && _value.$type === "json";
 | 
			
		||||
          const isFee = field === "Fee";
 | 
			
		||||
          let rows = isJson
 | 
			
		||||
            ? (value?.match(/\n/gm)?.length || 0) + 1
 | 
			
		||||
            : undefined;
 | 
			
		||||
          if (rows && rows > 5) rows = 5;
 | 
			
		||||
          return (
 | 
			
		||||
            <Flex column key={field} css={{ mb: "$2", pr: "1px" }}>
 | 
			
		||||
              <Flex
 | 
			
		||||
                row
 | 
			
		||||
                fluid
 | 
			
		||||
                css={{
 | 
			
		||||
                  justifyContent: "flex-end",
 | 
			
		||||
                  alignItems: "center",
 | 
			
		||||
                  position: "relative",
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Text muted css={{ mr: "$3" }}>
 | 
			
		||||
                  {field + (isXrp ? " (XRP)" : "")}:{" "}
 | 
			
		||||
                </Text>
 | 
			
		||||
                {isJson ? (
 | 
			
		||||
                  <Textarea
 | 
			
		||||
                    rows={rows}
 | 
			
		||||
                    value={value}
 | 
			
		||||
                    spellCheck={false}
 | 
			
		||||
                    onChange={switchToJson}
 | 
			
		||||
                    css={{
 | 
			
		||||
                      width: "70%",
 | 
			
		||||
                      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={{
 | 
			
		||||
                      width: "70%",
 | 
			
		||||
                      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>
 | 
			
		||||
                )}
 | 
			
		||||
              </Flex>
 | 
			
		||||
            </Flex>
 | 
			
		||||
          );
 | 
			
		||||
        })}
 | 
			
		||||
      </Flex>
 | 
			
		||||
    </Container>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										40
									
								
								components/icons/Carbon.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								components/icons/Carbon.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
const Carbon = () => (
 | 
			
		||||
  <svg
 | 
			
		||||
    width="66"
 | 
			
		||||
    height="32"
 | 
			
		||||
    viewBox="0 0 66 32"
 | 
			
		||||
    fill="none"
 | 
			
		||||
    xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
  >
 | 
			
		||||
    <path
 | 
			
		||||
      d="M33 2L23 15H28L21 24H45L38 15H43L33 2Z"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M33 24V30"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      className="angle"
 | 
			
		||||
      fillRule="evenodd"
 | 
			
		||||
      clipRule="evenodd"
 | 
			
		||||
      d="M-1.14441e-05 4L8.94099 15.0625L4.00543e-05 26.125H2.27587L10.5015 15.9475H16.5938V14.1775H10.5015L2.27582 4H-1.14441e-05Z"
 | 
			
		||||
      fill="#EDEDEF"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      className="angle"
 | 
			
		||||
      fillRule="evenodd"
 | 
			
		||||
      clipRule="evenodd"
 | 
			
		||||
      d="M66 4L57.059 15.0625L66 26.125H63.7241L55.4985 15.9475H49.4062V14.1775H55.4985L63.7242 4H66Z"
 | 
			
		||||
      fill="#EDEDEF"
 | 
			
		||||
    />
 | 
			
		||||
  </svg>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default Carbon;
 | 
			
		||||
							
								
								
									
										75
									
								
								components/icons/Firewall.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								components/icons/Firewall.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
const Firewall = () => (
 | 
			
		||||
  <svg
 | 
			
		||||
    width="66"
 | 
			
		||||
    height="32"
 | 
			
		||||
    viewBox="0 0 66 32"
 | 
			
		||||
    fill="none"
 | 
			
		||||
    xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
  >
 | 
			
		||||
    <path
 | 
			
		||||
      d="M33 13V7"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M27 19V13"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M39 19V13"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M33 25V19"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M21 13H45"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M21 19H45"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M45 7H21V25H45V7Z"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      className="angle"
 | 
			
		||||
      fillRule="evenodd"
 | 
			
		||||
      clipRule="evenodd"
 | 
			
		||||
      d="M-1.14441e-05 4.875L8.94099 15.9375L4.00543e-05 27H2.27587L10.5015 16.8225H16.5938V15.0525H10.5015L2.27582 4.875H-1.14441e-05Z"
 | 
			
		||||
      fill="#EDEDEF"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      className="angle"
 | 
			
		||||
      fillRule="evenodd"
 | 
			
		||||
      clipRule="evenodd"
 | 
			
		||||
      d="M66 4.875L57.059 15.9375L66 27H63.7241L55.4985 16.8225H49.4062V15.0525H55.4985L63.7242 4.875H66Z"
 | 
			
		||||
      fill="#EDEDEF"
 | 
			
		||||
    />
 | 
			
		||||
  </svg>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default Firewall;
 | 
			
		||||
							
								
								
									
										40
									
								
								components/icons/Notary.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								components/icons/Notary.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
const Notary = () => (
 | 
			
		||||
  <svg
 | 
			
		||||
    width="66"
 | 
			
		||||
    height="32"
 | 
			
		||||
    viewBox="0 0 66 32"
 | 
			
		||||
    fill="none"
 | 
			
		||||
    xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
  >
 | 
			
		||||
    <path
 | 
			
		||||
      d="M37.5 10.5L26.5 21.5L21 16.0002"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M49 10.5L38 21.5L35.0784 18.5785"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      className="angle"
 | 
			
		||||
      fillRule="evenodd"
 | 
			
		||||
      clipRule="evenodd"
 | 
			
		||||
      d="M-1.14441e-05 5L8.94099 16.0625L4.00543e-05 27.125H2.27587L10.5015 16.9475H16.5938V15.1775H10.5015L2.27582 5H-1.14441e-05Z"
 | 
			
		||||
      fill="#EDEDEF"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      className="angle"
 | 
			
		||||
      fillRule="evenodd"
 | 
			
		||||
      clipRule="evenodd"
 | 
			
		||||
      d="M66 5L57.059 16.0625L66 27.125H63.7241L55.4985 16.9475H49.4062V15.1775H55.4985L63.7242 5H66Z"
 | 
			
		||||
      fill="#EDEDEF"
 | 
			
		||||
    />
 | 
			
		||||
  </svg>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default Notary;
 | 
			
		||||
							
								
								
									
										61
									
								
								components/icons/Peggy.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								components/icons/Peggy.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
const Peggy = () => (
 | 
			
		||||
  <svg
 | 
			
		||||
    width="66"
 | 
			
		||||
    height="32"
 | 
			
		||||
    viewBox="0 0 66 32"
 | 
			
		||||
    fill="none"
 | 
			
		||||
    xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
  >
 | 
			
		||||
    <path
 | 
			
		||||
      d="M33 19C40.1797 19 46 16.3137 46 13C46 9.68629 40.1797 7 33 7C25.8203 7 20 9.68629 20 13C20 16.3137 25.8203 19 33 19Z"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M33 19V25"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M20 13V19C20 22 25 25 33 25C41 25 46 22 46 19V13"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M41 17.7633V23.7634"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M25 17.7633V23.7634"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      className="angle"
 | 
			
		||||
      fillRule="evenodd"
 | 
			
		||||
      clipRule="evenodd"
 | 
			
		||||
      d="M-1.14441e-05 4L8.94099 15.0625L4.00543e-05 26.125H2.27587L10.5015 15.9475H16.5938V14.1775H10.5015L2.27582 4H-1.14441e-05Z"
 | 
			
		||||
      fill="#EDEDEF"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      className="angle"
 | 
			
		||||
      fillRule="evenodd"
 | 
			
		||||
      clipRule="evenodd"
 | 
			
		||||
      d="M66 4L57.059 15.0625L66 26.125H63.7241L55.4985 15.9475H49.4062V14.1775H55.4985L63.7242 4H66Z"
 | 
			
		||||
      fill="#EDEDEF"
 | 
			
		||||
    />
 | 
			
		||||
  </svg>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default Peggy;
 | 
			
		||||
							
								
								
									
										40
									
								
								components/icons/Starter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								components/icons/Starter.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
const Starter = () => (
 | 
			
		||||
  <svg
 | 
			
		||||
    width="66"
 | 
			
		||||
    height="32"
 | 
			
		||||
    viewBox="0 0 66 32"
 | 
			
		||||
    fill="none"
 | 
			
		||||
    xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
  >
 | 
			
		||||
    <path
 | 
			
		||||
      d="M42 28H24C23.7347 28 23.4804 27.8946 23.2929 27.7071C23.1053 27.5196 23 27.2652 23 27V5C23 4.73479 23.1053 4.48044 23.2929 4.2929C23.4804 4.10537 23.7347 4.00001 24 4H36.0003L43 11V27C43 27.2652 42.8947 27.5196 42.7071 27.7071C42.5196 27.8946 42.2653 28 42 28V28Z"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M36 4V11H43.001"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      className="angle"
 | 
			
		||||
      fillRule="evenodd"
 | 
			
		||||
      clipRule="evenodd"
 | 
			
		||||
      d="M-1.14441e-05 4.875L8.94099 15.9375L4.00543e-05 27H2.27587L10.5015 16.8225H16.5938V15.0525H10.5015L2.27582 4.875H-1.14441e-05Z"
 | 
			
		||||
      fill="#EDEDEF"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      className="angle"
 | 
			
		||||
      fillRule="evenodd"
 | 
			
		||||
      clipRule="evenodd"
 | 
			
		||||
      d="M66 4.875L57.059 15.9375L66 27H63.7241L55.4985 16.8225H49.4062V15.0525H55.4985L63.7242 4.875H66Z"
 | 
			
		||||
      fill="#EDEDEF"
 | 
			
		||||
    />
 | 
			
		||||
  </svg>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default Starter;
 | 
			
		||||
@@ -4,14 +4,13 @@ export { default as Container } from "./Container";
 | 
			
		||||
export { default as Heading } from "./Heading";
 | 
			
		||||
export { default as Stack } from "./Stack";
 | 
			
		||||
export { default as Text } from "./Text";
 | 
			
		||||
export { default as Input } from "./Input";
 | 
			
		||||
export { default as Input, Label } from "./Input";
 | 
			
		||||
export { default as Select } from "./Select";
 | 
			
		||||
export * from "./Tabs";
 | 
			
		||||
export * from "./AlertDialog";
 | 
			
		||||
export * from "./AlertDialog/primitive";
 | 
			
		||||
export { default as Box } from "./Box";
 | 
			
		||||
export { default as Button } from "./Button";
 | 
			
		||||
export { default as Pre } from "./Pre";
 | 
			
		||||
export { default as ButtonGroup } from "./ButtonGroup";
 | 
			
		||||
export { default as DeployFooter } from "./DeployFooter";
 | 
			
		||||
export * from "./Dialog";
 | 
			
		||||
export * from "./DropdownMenu";
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										50
									
								
								content/amount-schema.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								content/amount-schema.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
			
		||||
{
 | 
			
		||||
  "uri": "file:///amount-schema.json",
 | 
			
		||||
  "title": "Amount",
 | 
			
		||||
  "description": "Specify xrp in drops and tokens as objects.",
 | 
			
		||||
  "schema": {
 | 
			
		||||
    "anyOf": [
 | 
			
		||||
      {
 | 
			
		||||
        "type": [
 | 
			
		||||
          "number",
 | 
			
		||||
          "string"
 | 
			
		||||
        ],
 | 
			
		||||
        "exclusiveMinimum": 0,
 | 
			
		||||
        "maximum": "100000000000000000"
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "type": "object",
 | 
			
		||||
        "properties": {
 | 
			
		||||
          "currency": {
 | 
			
		||||
            "description": "Arbitrary currency code for the token. Cannot be XRP."
 | 
			
		||||
          },
 | 
			
		||||
          "value": {
 | 
			
		||||
            "type": [
 | 
			
		||||
              "string",
 | 
			
		||||
              "number"
 | 
			
		||||
            ],
 | 
			
		||||
            "description": "Quoted decimal representation of the amount of the token."
 | 
			
		||||
          },
 | 
			
		||||
          "issuer": {
 | 
			
		||||
            "type": "string",
 | 
			
		||||
            "description": "Generally, the account that issues this token. In special cases, this can refer to the account that holds the token instead."
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "defaultSnippets": [
 | 
			
		||||
      {
 | 
			
		||||
        "label": "Xrp",
 | 
			
		||||
        "body": "1000000"
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "label": "Token",
 | 
			
		||||
        "body": {
 | 
			
		||||
          "currency": "${1:USD}",
 | 
			
		||||
          "value": "${2:100}",
 | 
			
		||||
          "issuer": "${3:rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpns}"
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -27,8 +27,8 @@
 | 
			
		||||
    "Account": "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy",
 | 
			
		||||
    "TransactionType": "CheckCash",
 | 
			
		||||
    "Amount": {
 | 
			
		||||
      "value": "100",
 | 
			
		||||
      "type": "currency"
 | 
			
		||||
      "$value": "100",
 | 
			
		||||
      "$type": "xrp"
 | 
			
		||||
    },
 | 
			
		||||
    "CheckID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334",
 | 
			
		||||
    "Fee": "12"
 | 
			
		||||
@@ -55,21 +55,23 @@
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "TransactionType": "EscrowCancel",
 | 
			
		||||
    "Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "OfferSequence": 7
 | 
			
		||||
    "OfferSequence": 7,
 | 
			
		||||
    "Fee": "10"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "TransactionType": "EscrowCreate",
 | 
			
		||||
    "Amount": {
 | 
			
		||||
      "value": "100",
 | 
			
		||||
      "type": "currency"
 | 
			
		||||
      "$value": "100",
 | 
			
		||||
      "$type": "xrp"
 | 
			
		||||
    },
 | 
			
		||||
    "Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
 | 
			
		||||
    "CancelAfter": 533257958,
 | 
			
		||||
    "FinishAfter": 533171558,
 | 
			
		||||
    "Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
 | 
			
		||||
    "DestinationTag": 23480,
 | 
			
		||||
    "SourceTag": 11747
 | 
			
		||||
    "SourceTag": 11747,
 | 
			
		||||
    "Fee": "10"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
@@ -77,32 +79,50 @@
 | 
			
		||||
    "Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "OfferSequence": 7,
 | 
			
		||||
    "Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
 | 
			
		||||
    "Fulfillment": "A0028000"
 | 
			
		||||
    "Fulfillment": "A0028000",
 | 
			
		||||
    "Fee": "10"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "NFTokenMint",
 | 
			
		||||
    "Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
 | 
			
		||||
    "Fee": "10",
 | 
			
		||||
    "NFTokenTaxon": 0,
 | 
			
		||||
    "URI": "697066733A2F2F516D614374444B5A4656767666756676626479346573745A626851483744586831364354707631686F776D424779"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "NFTokenBurn",
 | 
			
		||||
    "Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
 | 
			
		||||
    "Fee": "10",
 | 
			
		||||
    "TokenID": "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65"
 | 
			
		||||
    "NFTokenID": "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "NFTokenAcceptOffer",
 | 
			
		||||
    "Fee": "10"
 | 
			
		||||
    "Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
 | 
			
		||||
    "Fee": "10",
 | 
			
		||||
    "NFTokenSellOffer": "A2FA1A9911FE2AEF83DAB05F437768E26A301EF899BD31EB85E704B3D528FF18",
 | 
			
		||||
    "NFTokenBuyOffer": "4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862",
 | 
			
		||||
    "NFTokenBrokerFee": "10"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "NFTokenCancelOffer",
 | 
			
		||||
    "Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
 | 
			
		||||
    "TokenIDs": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007"
 | 
			
		||||
    "Fee": "10",
 | 
			
		||||
    "NFTokenOffers": {
 | 
			
		||||
      "$type": "json",
 | 
			
		||||
      "$value":  ["4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862"]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "NFTokenCreateOffer",
 | 
			
		||||
    "Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX",
 | 
			
		||||
    "TokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
 | 
			
		||||
    "NFTokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
 | 
			
		||||
    "Amount": {
 | 
			
		||||
      "value": "100",
 | 
			
		||||
      "type": "currency"
 | 
			
		||||
      "$value": "100",
 | 
			
		||||
      "$type": "xrp"
 | 
			
		||||
    },
 | 
			
		||||
    "Flags": 1
 | 
			
		||||
    "Flags": 1,
 | 
			
		||||
    "Destination": "",
 | 
			
		||||
    "Fee": "10"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "OfferCancel",
 | 
			
		||||
@@ -122,8 +142,8 @@
 | 
			
		||||
    "Sequence": 8,
 | 
			
		||||
    "TakerGets": "6000000",
 | 
			
		||||
    "Amount": {
 | 
			
		||||
      "value": "100",
 | 
			
		||||
      "type": "currency"
 | 
			
		||||
      "$value": "100",
 | 
			
		||||
      "$type": "xrp"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
@@ -131,8 +151,8 @@
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
 | 
			
		||||
    "Amount": {
 | 
			
		||||
      "value": "100",
 | 
			
		||||
      "type": "currency"
 | 
			
		||||
      "$value": "100",
 | 
			
		||||
      "$type": "xrp"
 | 
			
		||||
    },
 | 
			
		||||
    "Fee": "12",
 | 
			
		||||
    "Flags": 2147483648,
 | 
			
		||||
@@ -142,25 +162,27 @@
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "TransactionType": "PaymentChannelCreate",
 | 
			
		||||
    "Amount": {
 | 
			
		||||
      "value": "100",
 | 
			
		||||
      "type": "currency"
 | 
			
		||||
      "$value": "100",
 | 
			
		||||
      "$type": "xrp"
 | 
			
		||||
    },
 | 
			
		||||
    "Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
 | 
			
		||||
    "SettleDelay": 86400,
 | 
			
		||||
    "PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A",
 | 
			
		||||
    "CancelAfter": 533171558,
 | 
			
		||||
    "DestinationTag": 23480,
 | 
			
		||||
    "SourceTag": 11747
 | 
			
		||||
    "SourceTag": 11747,
 | 
			
		||||
    "Fee": "10"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "TransactionType": "PaymentChannelFund",
 | 
			
		||||
    "Channel": "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198",
 | 
			
		||||
    "Amount": {
 | 
			
		||||
      "value": "200",
 | 
			
		||||
      "type": "currency"
 | 
			
		||||
      "$value": "200",
 | 
			
		||||
      "$type": "xrp"
 | 
			
		||||
    },
 | 
			
		||||
    "Expiration": 543171558
 | 
			
		||||
    "Expiration": 543171558,
 | 
			
		||||
    "Fee": "10"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "Flags": 0,
 | 
			
		||||
@@ -176,8 +198,8 @@
 | 
			
		||||
    "Fee": "12",
 | 
			
		||||
    "SignerQuorum": 3,
 | 
			
		||||
    "SignerEntries": {
 | 
			
		||||
      "type": "json",
 | 
			
		||||
      "value": [
 | 
			
		||||
      "$type": "json",
 | 
			
		||||
      "$value": [
 | 
			
		||||
        {
 | 
			
		||||
          "SignerEntry": {
 | 
			
		||||
            "Account": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
 | 
			
		||||
@@ -212,9 +234,13 @@
 | 
			
		||||
    "Fee": "12",
 | 
			
		||||
    "Flags": 262144,
 | 
			
		||||
    "LastLedgerSequence": 8007750,
 | 
			
		||||
    "Amount": {
 | 
			
		||||
      "value": "100",
 | 
			
		||||
      "type": "currency"
 | 
			
		||||
    "LimitAmount": {
 | 
			
		||||
      "$type": "json",
 | 
			
		||||
      "$value": {
 | 
			
		||||
        "currency": "USD",
 | 
			
		||||
        "issuer": "rsP3mgGb2tcYUrxiLFiHJiQXhsziegtwBc",
 | 
			
		||||
        "value": "100"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "Sequence": 12
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								next-env.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								next-env.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -1,5 +1,4 @@
 | 
			
		||||
/// <reference types="next" />
 | 
			
		||||
/// <reference types="next/types/global" />
 | 
			
		||||
/// <reference types="next/image-types/global" />
 | 
			
		||||
 | 
			
		||||
// NOTE: This file should not be edited
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										29
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								package.json
									
									
									
									
									
								
							@@ -12,25 +12,33 @@
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@codingame/monaco-jsonrpc": "^0.3.1",
 | 
			
		||||
    "@codingame/monaco-languageclient": "^0.17.0",
 | 
			
		||||
    "@monaco-editor/react": "^4.3.1",
 | 
			
		||||
    "@monaco-editor/react": "^4.4.5",
 | 
			
		||||
    "@octokit/core": "^3.5.1",
 | 
			
		||||
    "@radix-ui/colors": "^0.1.7",
 | 
			
		||||
    "@radix-ui/react-alert-dialog": "^0.1.1",
 | 
			
		||||
    "@radix-ui/react-context-menu": "^0.1.6",
 | 
			
		||||
    "@radix-ui/react-dialog": "^0.1.1",
 | 
			
		||||
    "@radix-ui/react-dropdown-menu": "^0.1.1",
 | 
			
		||||
    "@radix-ui/react-dropdown-menu": "^0.1.6",
 | 
			
		||||
    "@radix-ui/react-id": "^0.1.1",
 | 
			
		||||
    "@stitches/react": "^1.2.6-0",
 | 
			
		||||
    "@radix-ui/react-label": "^0.1.5",
 | 
			
		||||
    "@radix-ui/react-popover": "^0.1.6",
 | 
			
		||||
    "@radix-ui/react-switch": "^0.1.5",
 | 
			
		||||
    "@radix-ui/react-tooltip": "^0.1.7",
 | 
			
		||||
    "@stitches/react": "^1.2.8",
 | 
			
		||||
    "base64-js": "^1.5.1",
 | 
			
		||||
    "comment-parser": "^1.3.1",
 | 
			
		||||
    "dinero.js": "^1.9.1",
 | 
			
		||||
    "file-saver": "^2.0.5",
 | 
			
		||||
    "filesize": "^8.0.7",
 | 
			
		||||
    "javascript-time-ago": "^2.3.11",
 | 
			
		||||
    "jszip": "^3.7.1",
 | 
			
		||||
    "lodash.uniqby": "^4.7.0",
 | 
			
		||||
    "monaco-editor": "^0.30.1",
 | 
			
		||||
    "lodash.xor": "^4.5.0",
 | 
			
		||||
    "monaco-editor": "^0.33.0",
 | 
			
		||||
    "next": "^12.0.4",
 | 
			
		||||
    "next-auth": "^4.0.0-beta.5",
 | 
			
		||||
    "next-themes": "^0.0.15",
 | 
			
		||||
    "next-auth": "^4.10.1",
 | 
			
		||||
    "next-plausible": "^3.2.0",
 | 
			
		||||
    "next-themes": "^0.1.1",
 | 
			
		||||
    "normalize-url": "^7.0.2",
 | 
			
		||||
    "octokit": "^1.7.0",
 | 
			
		||||
    "pako": "^2.0.4",
 | 
			
		||||
@@ -40,6 +48,7 @@
 | 
			
		||||
    "re-resizable": "^6.9.1",
 | 
			
		||||
    "react": "17.0.2",
 | 
			
		||||
    "react-dom": "17.0.2",
 | 
			
		||||
    "react-hook-form": "^7.28.0",
 | 
			
		||||
    "react-hot-keys": "^2.7.1",
 | 
			
		||||
    "react-hot-toast": "^2.1.1",
 | 
			
		||||
    "react-new-window": "^0.2.1",
 | 
			
		||||
@@ -53,18 +62,22 @@
 | 
			
		||||
    "vscode-languageserver": "^7.0.0",
 | 
			
		||||
    "vscode-uri": "^3.0.2",
 | 
			
		||||
    "wabt": "1.0.16",
 | 
			
		||||
    "xrpl-accountlib": "^1.2.3",
 | 
			
		||||
    "xrpl-client": "^1.9.3"
 | 
			
		||||
    "xrpl-accountlib": "^1.5.2",
 | 
			
		||||
    "xrpl-client": "^1.9.4"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@types/dinero.js": "^1.9.0",
 | 
			
		||||
    "@types/file-saver": "^2.0.4",
 | 
			
		||||
    "@types/lodash.uniqby": "^4.7.6",
 | 
			
		||||
    "@types/lodash.xor": "^4.5.6",
 | 
			
		||||
    "@types/pako": "^1.0.2",
 | 
			
		||||
    "@types/react": "17.0.31",
 | 
			
		||||
    "eslint": "7.32.0",
 | 
			
		||||
    "eslint-config-next": "11.1.2",
 | 
			
		||||
    "raw-loader": "^4.0.2",
 | 
			
		||||
    "typescript": "4.4.4"
 | 
			
		||||
  },
 | 
			
		||||
  "resolutions": {
 | 
			
		||||
    "ripple-binary-codec": "=1.4.2"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										104
									
								
								pages/_app.tsx
									
									
									
									
									
								
							
							
						
						
									
										104
									
								
								pages/_app.tsx
									
									
									
									
									
								
							@@ -7,6 +7,7 @@ import { ThemeProvider } from "next-themes";
 | 
			
		||||
import { Toaster } from "react-hot-toast";
 | 
			
		||||
import { useRouter } from "next/router";
 | 
			
		||||
import { IdProvider } from "@radix-ui/react-id";
 | 
			
		||||
import PlausibleProvider from "next-plausible";
 | 
			
		||||
 | 
			
		||||
import { darkTheme, css } from "../stitches.config";
 | 
			
		||||
import Navigation from "../components/Navigation";
 | 
			
		||||
@@ -15,7 +16,13 @@ import state from "../state";
 | 
			
		||||
 | 
			
		||||
import TimeAgo from "javascript-time-ago";
 | 
			
		||||
import en from "javascript-time-ago/locale/en.json";
 | 
			
		||||
TimeAgo.addDefaultLocale(en);
 | 
			
		||||
import { useSnapshot } from "valtio";
 | 
			
		||||
import Alert from "../components/AlertDialog";
 | 
			
		||||
import { Button, Flex } from "../components";
 | 
			
		||||
import { ChatCircleText } from "phosphor-react";
 | 
			
		||||
 | 
			
		||||
TimeAgo.setDefaultLocale(en.locale);
 | 
			
		||||
TimeAgo.addLocale(en);
 | 
			
		||||
 | 
			
		||||
function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
@@ -25,15 +32,29 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
 | 
			
		||||
  const origin = "https://xrpl-hooks-ide.vercel.app"; // TODO: Change when site is deployed
 | 
			
		||||
  const shareImg = "/share-image.png";
 | 
			
		||||
 | 
			
		||||
  const snap = useSnapshot(state);
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (gistId && router.isReady) {
 | 
			
		||||
      fetchFiles(gistId);
 | 
			
		||||
    } else {
 | 
			
		||||
      if (!gistId && router.isReady && !router.pathname.includes("/sign-in")) {
 | 
			
		||||
      if (
 | 
			
		||||
        !gistId &&
 | 
			
		||||
        router.isReady &&
 | 
			
		||||
        router.pathname.includes("/develop") &&
 | 
			
		||||
        !snap.files.length &&
 | 
			
		||||
        !snap.mainModalShowed
 | 
			
		||||
      ) {
 | 
			
		||||
        state.mainModalOpen = true;
 | 
			
		||||
        state.mainModalShowed = true;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, [gistId, router.isReady, router.pathname]);
 | 
			
		||||
  }, [
 | 
			
		||||
    gistId,
 | 
			
		||||
    router.isReady,
 | 
			
		||||
    router.pathname,
 | 
			
		||||
    snap.files,
 | 
			
		||||
    snap.mainModalShowed,
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
@@ -43,22 +64,22 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
 | 
			
		||||
        <meta name="format-detection" content="telephone=no" />
 | 
			
		||||
        <meta property="og:url" content={`${origin}${router.asPath}`} />
 | 
			
		||||
 | 
			
		||||
        <title>XRPL Hooks Editor</title>
 | 
			
		||||
        <meta property="og:title" content="XRPL Hooks Editor" />
 | 
			
		||||
        <meta name="twitter:title" content="XRPL Hooks Editor" />
 | 
			
		||||
        <title>XRPL Hooks Builder</title>
 | 
			
		||||
        <meta property="og:title" content="XRPL Hooks Builder" />
 | 
			
		||||
        <meta name="twitter:title" content="XRPL Hooks Builder" />
 | 
			
		||||
        <meta name="twitter:card" content="summary_large_image" />
 | 
			
		||||
        <meta name="twitter:site" content="@xrpllabs" />
 | 
			
		||||
        <meta name="twitter:site" content="@XRPLF" />
 | 
			
		||||
        <meta
 | 
			
		||||
          name="description"
 | 
			
		||||
          content="Playground for buildings Hooks, that add smart contract functionality to the XRP Ledger."
 | 
			
		||||
          content="Hooks Builder, add smart contract functionality to the XRP Ledger."
 | 
			
		||||
        />
 | 
			
		||||
        <meta
 | 
			
		||||
          property="og:description"
 | 
			
		||||
          content="Playground for buildings Hooks, that add smart contract functionality to the XRP Ledger."
 | 
			
		||||
          content="Hooks Builder, add smart contract functionality to the XRP Ledger."
 | 
			
		||||
        />
 | 
			
		||||
        <meta
 | 
			
		||||
          name="twitter:description"
 | 
			
		||||
          content="Playground for buildings Hooks, that add smart contract functionality to the XRP Ledger.."
 | 
			
		||||
          content="Hooks Builder, add smart contract functionality to the XRP Ledger."
 | 
			
		||||
        />
 | 
			
		||||
        <meta property="og:image" content={`${origin}${shareImg}`} />
 | 
			
		||||
        <meta property="og:image:width" content="1200" />
 | 
			
		||||
@@ -83,7 +104,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
 | 
			
		||||
        />
 | 
			
		||||
        <link rel="manifest" href="/site.webmanifest" />
 | 
			
		||||
        <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#161618" />
 | 
			
		||||
        <meta name="application-name" content="XRPL Hooks Editor" />
 | 
			
		||||
        <meta name="application-name" content="XRPL Hooks Builder" />
 | 
			
		||||
        <meta name="msapplication-TileColor" content="#c10ad0" />
 | 
			
		||||
        <meta
 | 
			
		||||
          name="theme-color"
 | 
			
		||||
@@ -95,17 +116,8 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
 | 
			
		||||
          content="#FDFCFD"
 | 
			
		||||
          media="(prefers-color-scheme: light)"
 | 
			
		||||
        />
 | 
			
		||||
        <link rel="preconnect" href="https://fonts.googleapis.com" />
 | 
			
		||||
        <link
 | 
			
		||||
          rel="preconnect"
 | 
			
		||||
          href="https://fonts.gstatic.com"
 | 
			
		||||
          crossOrigin=""
 | 
			
		||||
        />
 | 
			
		||||
        <link
 | 
			
		||||
          href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital@0;1&family=Work+Sans:wght@400;600;700&display=swap"
 | 
			
		||||
          rel="stylesheet"
 | 
			
		||||
        />
 | 
			
		||||
      </Head>
 | 
			
		||||
 | 
			
		||||
      <IdProvider>
 | 
			
		||||
        <SessionProvider session={session}>
 | 
			
		||||
          <ThemeProvider
 | 
			
		||||
@@ -117,22 +129,40 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
 | 
			
		||||
              dark: darkTheme.className,
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Navigation />
 | 
			
		||||
            <Component {...pageProps} />
 | 
			
		||||
            <Toaster
 | 
			
		||||
              toastOptions={{
 | 
			
		||||
                className: css({
 | 
			
		||||
                  backgroundColor: "$mauve1",
 | 
			
		||||
                  color: "$mauve10",
 | 
			
		||||
                  fontSize: "$sm",
 | 
			
		||||
                  zIndex: 9999,
 | 
			
		||||
                  ".dark &": {
 | 
			
		||||
                    backgroundColor: "$mauve4",
 | 
			
		||||
                    color: "$mauve12",
 | 
			
		||||
                  },
 | 
			
		||||
                })(),
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
            <PlausibleProvider
 | 
			
		||||
              domain="hooks-builder.xrpl.org"
 | 
			
		||||
              trackOutboundLinks
 | 
			
		||||
            >
 | 
			
		||||
              <Navigation />
 | 
			
		||||
              <Component {...pageProps} />
 | 
			
		||||
              <Toaster
 | 
			
		||||
                toastOptions={{
 | 
			
		||||
                  className: css({
 | 
			
		||||
                    backgroundColor: "$mauve1",
 | 
			
		||||
                    color: "$mauve10",
 | 
			
		||||
                    fontSize: "$sm",
 | 
			
		||||
                    zIndex: 9999,
 | 
			
		||||
                    ".dark &": {
 | 
			
		||||
                      backgroundColor: "$mauve4",
 | 
			
		||||
                      color: "$mauve12",
 | 
			
		||||
                    },
 | 
			
		||||
                  })(),
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
              <Alert />
 | 
			
		||||
              <Flex
 | 
			
		||||
                as="a"
 | 
			
		||||
                href="https://github.com/XRPLF/Hooks/discussions"
 | 
			
		||||
                target="_blank"
 | 
			
		||||
                rel="noopener noreferrer"
 | 
			
		||||
                css={{ position: "fixed", right: "$4", bottom: "$4" }}
 | 
			
		||||
              >
 | 
			
		||||
                <Button size="sm" variant="primary" outline>
 | 
			
		||||
                  <ChatCircleText size={14} style={{ marginRight: "0px" }} />
 | 
			
		||||
                  Bugs & Discussions
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Flex>
 | 
			
		||||
            </PlausibleProvider>
 | 
			
		||||
          </ThemeProvider>
 | 
			
		||||
        </SessionProvider>
 | 
			
		||||
      </IdProvider>
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,16 @@ class MyDocument extends Document {
 | 
			
		||||
            id="stitches"
 | 
			
		||||
            dangerouslySetInnerHTML={{ __html: getCssText() }}
 | 
			
		||||
          />
 | 
			
		||||
          <link rel="preconnect" href="https://fonts.googleapis.com" />
 | 
			
		||||
          <link
 | 
			
		||||
            rel="preconnect"
 | 
			
		||||
            href="https://fonts.gstatic.com"
 | 
			
		||||
            crossOrigin=""
 | 
			
		||||
          />
 | 
			
		||||
          <link
 | 
			
		||||
            href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital@0;1&family=Work+Sans:wght@400;600;700&display=swap"
 | 
			
		||||
            rel="stylesheet"
 | 
			
		||||
          />
 | 
			
		||||
        </Head>
 | 
			
		||||
        <body>
 | 
			
		||||
          <Main />
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,9 @@ import { NextResponse as Response } from 'next/server';
 | 
			
		||||
export default function middleware(req: NextRequest, ev: NextFetchEvent) {
 | 
			
		||||
 | 
			
		||||
  if (req.nextUrl.pathname === "/") {
 | 
			
		||||
    return Response.redirect("/develop");
 | 
			
		||||
    const url = req.nextUrl.clone();
 | 
			
		||||
    url.pathname = '/develop';
 | 
			
		||||
    return Response.redirect(url);
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -21,8 +21,15 @@ export default async function handler(
 | 
			
		||||
  if (req.method !== 'POST') {
 | 
			
		||||
    return res.status(405).json({ error: 'Method not allowed!' })
 | 
			
		||||
  }
 | 
			
		||||
  const { account } = req.query;
 | 
			
		||||
  const ip = Array.isArray(req?.headers?.["x-real-ip"]) ? req?.headers?.["x-real-ip"][0] : req?.headers?.["x-real-ip"];
 | 
			
		||||
  try {
 | 
			
		||||
    const response = await fetch('https://hooks-testnet.xrpl-labs.com/newcreds', { method: 'POST' });
 | 
			
		||||
    const response = await fetch(`https://${process.env.NEXT_PUBLIC_TESTNET_URL}/newcreds?account=${account ? account : ''}`, {
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      headers: {
 | 
			
		||||
        'x-forwarded-for': ip || '',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    const json: Faucet | ErrorResponse = await response.json();
 | 
			
		||||
    if ("error" in json) {
 | 
			
		||||
      return res.status(429).json(json)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								pages/api/proxy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								pages/api/proxy.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
import type { NextApiRequest, NextApiResponse } from 'next'
 | 
			
		||||
 | 
			
		||||
export default async function handler(
 | 
			
		||||
    req: NextApiRequest,
 | 
			
		||||
    res: NextApiResponse
 | 
			
		||||
) {
 | 
			
		||||
    try {
 | 
			
		||||
        const { url, opts } = req.body
 | 
			
		||||
        const r = await fetch(url, opts);
 | 
			
		||||
        if (!r.ok) throw (r.statusText)
 | 
			
		||||
 | 
			
		||||
        const data = await r.json()
 | 
			
		||||
        return res.json(data)
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        console.warn(error)
 | 
			
		||||
        return res.status(500).json({ message: "Something went wrong!" })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,15 +1,19 @@
 | 
			
		||||
import { Label } from "@radix-ui/react-label";
 | 
			
		||||
import type { NextPage } from "next";
 | 
			
		||||
import dynamic from "next/dynamic";
 | 
			
		||||
import { Play } from "phosphor-react";
 | 
			
		||||
import { FileJs, Gear, Play } from "phosphor-react";
 | 
			
		||||
import Hotkeys from "react-hot-keys";
 | 
			
		||||
import Split from "react-split";
 | 
			
		||||
import { useSnapshot } from "valtio";
 | 
			
		||||
import { ButtonGroup, Flex } from "../../components";
 | 
			
		||||
import Box from "../../components/Box";
 | 
			
		||||
import Button from "../../components/Button";
 | 
			
		||||
import Popover from "../../components/Popover";
 | 
			
		||||
import RunScript from "../../components/RunScript";
 | 
			
		||||
import state from "../../state";
 | 
			
		||||
import { compileCode } from "../../state/actions";
 | 
			
		||||
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
 | 
			
		||||
 | 
			
		||||
import { styled } from "../../stitches.config";
 | 
			
		||||
 | 
			
		||||
const HooksEditor = dynamic(() => import("../../components/HooksEditor"), {
 | 
			
		||||
  ssr: false,
 | 
			
		||||
@@ -19,6 +23,128 @@ const LogBox = dynamic(() => import("../../components/LogBox"), {
 | 
			
		||||
  ssr: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const OptimizationText = () => (
 | 
			
		||||
  <span>
 | 
			
		||||
    Specify which optimization level to use for compiling. For example -O0 means
 | 
			
		||||
    “no optimization”: this level compiles the fastest and generates the most
 | 
			
		||||
    debuggable code. -O2 means moderate level of optimization which enables most
 | 
			
		||||
    optimizations. Read more about the options from{" "}
 | 
			
		||||
    <a
 | 
			
		||||
      className="link"
 | 
			
		||||
      rel="noopener noreferrer"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      href="https://clang.llvm.org/docs/CommandGuide/clang.html#cmdoption-o0"
 | 
			
		||||
    >
 | 
			
		||||
      clang documentation
 | 
			
		||||
    </a>
 | 
			
		||||
    .
 | 
			
		||||
  </span>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const StyledOptimizationText = styled(OptimizationText, {
 | 
			
		||||
  color: "$mauve12 !important",
 | 
			
		||||
  fontSize: "200px",
 | 
			
		||||
  "span a.link": {
 | 
			
		||||
    color: "red",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const CompilerSettings = () => {
 | 
			
		||||
  const snap = useSnapshot(state);
 | 
			
		||||
  return (
 | 
			
		||||
    <Flex css={{ minWidth: 200, flexDirection: "column", gap: "$5" }}>
 | 
			
		||||
      <Box>
 | 
			
		||||
        <Label
 | 
			
		||||
          style={{
 | 
			
		||||
            flexDirection: "row",
 | 
			
		||||
            display: "flex",
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          Optimization level{" "}
 | 
			
		||||
          <Popover
 | 
			
		||||
            css={{
 | 
			
		||||
              maxWidth: "240px",
 | 
			
		||||
              lineHeight: "1.3",
 | 
			
		||||
              a: {
 | 
			
		||||
                color: "$purple11",
 | 
			
		||||
              },
 | 
			
		||||
              ".dark &": {
 | 
			
		||||
                backgroundColor: "$black !important",
 | 
			
		||||
 | 
			
		||||
                ".arrow": {
 | 
			
		||||
                  fill: "$colors$black",
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            }}
 | 
			
		||||
            content={<StyledOptimizationText />}
 | 
			
		||||
          >
 | 
			
		||||
            <Flex
 | 
			
		||||
              css={{
 | 
			
		||||
                position: "relative",
 | 
			
		||||
                top: "-1px",
 | 
			
		||||
                ml: "$1",
 | 
			
		||||
                backgroundColor: "$mauve8",
 | 
			
		||||
                borderRadius: "$full",
 | 
			
		||||
                cursor: "pointer",
 | 
			
		||||
                width: "16px",
 | 
			
		||||
                height: "16px",
 | 
			
		||||
                alignItems: "center",
 | 
			
		||||
                justifyContent: "center",
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              ?
 | 
			
		||||
            </Flex>
 | 
			
		||||
          </Popover>
 | 
			
		||||
        </Label>
 | 
			
		||||
        <ButtonGroup css={{ mt: "$2", fontFamily: "$monospace" }}>
 | 
			
		||||
          <Button
 | 
			
		||||
            css={{ fontFamily: "$monospace" }}
 | 
			
		||||
            outline={snap.compileOptions.optimizationLevel !== "-O0"}
 | 
			
		||||
            onClick={() => (state.compileOptions.optimizationLevel = "-O0")}
 | 
			
		||||
          >
 | 
			
		||||
            -O0
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button
 | 
			
		||||
            css={{ fontFamily: "$monospace" }}
 | 
			
		||||
            outline={snap.compileOptions.optimizationLevel !== "-O1"}
 | 
			
		||||
            onClick={() => (state.compileOptions.optimizationLevel = "-O1")}
 | 
			
		||||
          >
 | 
			
		||||
            -O1
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button
 | 
			
		||||
            css={{ fontFamily: "$monospace" }}
 | 
			
		||||
            outline={snap.compileOptions.optimizationLevel !== "-O2"}
 | 
			
		||||
            onClick={() => (state.compileOptions.optimizationLevel = "-O2")}
 | 
			
		||||
          >
 | 
			
		||||
            -O2
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button
 | 
			
		||||
            css={{ fontFamily: "$monospace" }}
 | 
			
		||||
            outline={snap.compileOptions.optimizationLevel !== "-O3"}
 | 
			
		||||
            onClick={() => (state.compileOptions.optimizationLevel = "-O3")}
 | 
			
		||||
          >
 | 
			
		||||
            -O3
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button
 | 
			
		||||
            css={{ fontFamily: "$monospace" }}
 | 
			
		||||
            outline={snap.compileOptions.optimizationLevel !== "-O4"}
 | 
			
		||||
            onClick={() => (state.compileOptions.optimizationLevel = "-O4")}
 | 
			
		||||
          >
 | 
			
		||||
            -O4
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button
 | 
			
		||||
            css={{ fontFamily: "$monospace" }}
 | 
			
		||||
            outline={snap.compileOptions.optimizationLevel !== "-Os"}
 | 
			
		||||
            onClick={() => (state.compileOptions.optimizationLevel = "-Os")}
 | 
			
		||||
          >
 | 
			
		||||
            -Os
 | 
			
		||||
          </Button>
 | 
			
		||||
        </ButtonGroup>
 | 
			
		||||
      </Box>
 | 
			
		||||
    </Flex>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const Home: NextPage = () => {
 | 
			
		||||
  const snap = useSnapshot(state);
 | 
			
		||||
 | 
			
		||||
@@ -34,7 +160,7 @@ const Home: NextPage = () => {
 | 
			
		||||
    >
 | 
			
		||||
      <main style={{ display: "flex", flex: 1, position: "relative" }}>
 | 
			
		||||
        <HooksEditor />
 | 
			
		||||
        {snap.files[snap.active]?.name?.split(".")?.[1].toLowerCase() ===
 | 
			
		||||
        {snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() ===
 | 
			
		||||
          "c" && (
 | 
			
		||||
          <Hotkeys
 | 
			
		||||
            keyName="command+b,ctrl+b"
 | 
			
		||||
@@ -42,12 +168,7 @@ const Home: NextPage = () => {
 | 
			
		||||
              !snap.compiling && snap.files.length && compileCode(snap.active)
 | 
			
		||||
            }
 | 
			
		||||
          >
 | 
			
		||||
            <Button
 | 
			
		||||
              variant="primary"
 | 
			
		||||
              uppercase
 | 
			
		||||
              disabled={!snap.files.length}
 | 
			
		||||
              isLoading={snap.compiling}
 | 
			
		||||
              onClick={() => compileCode(snap.active)}
 | 
			
		||||
            <Flex
 | 
			
		||||
              css={{
 | 
			
		||||
                position: "absolute",
 | 
			
		||||
                bottom: "$4",
 | 
			
		||||
@@ -55,27 +176,82 @@ const Home: NextPage = () => {
 | 
			
		||||
                alignItems: "center",
 | 
			
		||||
                display: "flex",
 | 
			
		||||
                cursor: "pointer",
 | 
			
		||||
                gap: "$2",
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Play weight="bold" size="16px" />
 | 
			
		||||
              Compile to Wasm
 | 
			
		||||
            </Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                variant="primary"
 | 
			
		||||
                uppercase
 | 
			
		||||
                disabled={!snap.files.length}
 | 
			
		||||
                isLoading={snap.compiling}
 | 
			
		||||
                onClick={() => compileCode(snap.active)}
 | 
			
		||||
              >
 | 
			
		||||
                <Play weight="bold" size="16px" />
 | 
			
		||||
                Compile to Wasm
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Popover content={<CompilerSettings />}>
 | 
			
		||||
                <Button variant="primary" css={{ px: "10px" }}>
 | 
			
		||||
                  <Gear size="16px" />
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Popover>
 | 
			
		||||
            </Flex>
 | 
			
		||||
          </Hotkeys>
 | 
			
		||||
        )}
 | 
			
		||||
        {snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() ===
 | 
			
		||||
          "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={snap.files[snap.active]} />
 | 
			
		||||
            </Flex>
 | 
			
		||||
          </Hotkeys>
 | 
			
		||||
        )}
 | 
			
		||||
      </main>
 | 
			
		||||
      <Box
 | 
			
		||||
        css={{
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          background: "$mauve1",
 | 
			
		||||
          position: "relative",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <LogBox
 | 
			
		||||
          title="Development Log"
 | 
			
		||||
          clearLog={() => (state.logs = [])}
 | 
			
		||||
          logs={snap.logs}
 | 
			
		||||
        />
 | 
			
		||||
      </Box>
 | 
			
		||||
      <Flex css={{ width: "100%" }}>
 | 
			
		||||
        <Flex
 | 
			
		||||
          css={{
 | 
			
		||||
            flex: 1,
 | 
			
		||||
            background: "$mauve1",
 | 
			
		||||
            position: "relative",
 | 
			
		||||
            borderRight: "1px solid $mauve8",
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <LogBox
 | 
			
		||||
            title="Development Log"
 | 
			
		||||
            clearLog={() => (state.logs = [])}
 | 
			
		||||
            logs={snap.logs}
 | 
			
		||||
          />
 | 
			
		||||
        </Flex>
 | 
			
		||||
        {snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() ===
 | 
			
		||||
          "js" && (
 | 
			
		||||
          <Flex
 | 
			
		||||
            css={{
 | 
			
		||||
              flex: 1,
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <LogBox
 | 
			
		||||
              Icon={FileJs}
 | 
			
		||||
              title="Script Log"
 | 
			
		||||
              logs={snap.scriptLogs}
 | 
			
		||||
              clearLog={() => (state.scriptLogs = [])}
 | 
			
		||||
            />
 | 
			
		||||
          </Flex>
 | 
			
		||||
        )}
 | 
			
		||||
      </Flex>
 | 
			
		||||
    </Split>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,14 @@
 | 
			
		||||
import dynamic from "next/dynamic";
 | 
			
		||||
import { Play } from "phosphor-react";
 | 
			
		||||
import { FC, useCallback, useEffect, useState } from "react";
 | 
			
		||||
import Split from "react-split";
 | 
			
		||||
import { useSnapshot } from "valtio";
 | 
			
		||||
import {
 | 
			
		||||
  Box, Button, Container,
 | 
			
		||||
  Flex, Input,
 | 
			
		||||
  Select, Tab, Tabs, Text
 | 
			
		||||
} from "../../components";
 | 
			
		||||
import transactionsData from "../../content/transactions.json";
 | 
			
		||||
import state from "../../state";
 | 
			
		||||
import { sendTransaction } from "../../state/actions";
 | 
			
		||||
import { Box, Container, Flex, Tab, Tabs } from "../../components";
 | 
			
		||||
import Transaction from "../../components/Transaction";
 | 
			
		||||
import state, { renameTxState } from "../../state";
 | 
			
		||||
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
 | 
			
		||||
import { transactionsState, modifyTxState } from "../../state";
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { FileJs } from "phosphor-react";
 | 
			
		||||
import RunScript from "../../components/RunScript";
 | 
			
		||||
 | 
			
		||||
const DebugStream = dynamic(() => import("../../components/DebugStream"), {
 | 
			
		||||
  ssr: false,
 | 
			
		||||
@@ -24,337 +21,47 @@ const Accounts = dynamic(() => import("../../components/Accounts"), {
 | 
			
		||||
  ssr: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// type SelectOption<T> = { value: T, label: string };
 | 
			
		||||
type TxFields = Omit<
 | 
			
		||||
  typeof transactionsData[0],
 | 
			
		||||
  "Account" | "Sequence" | "TransactionType"
 | 
			
		||||
>;
 | 
			
		||||
type OtherFields = (keyof Omit<TxFields, "Destination">)[];
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  header?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Transaction: FC<Props> = ({ header, ...props }) => {
 | 
			
		||||
  const snap = useSnapshot(state);
 | 
			
		||||
 | 
			
		||||
  const transactionsOptions = transactionsData.map((tx) => ({
 | 
			
		||||
    value: tx.TransactionType,
 | 
			
		||||
    label: tx.TransactionType,
 | 
			
		||||
  }));
 | 
			
		||||
  const [selectedTransaction, setSelectedTransaction] = useState<
 | 
			
		||||
    typeof transactionsOptions[0] | null
 | 
			
		||||
  >(null);
 | 
			
		||||
 | 
			
		||||
  const accountOptions = snap.accounts.map((acc) => ({
 | 
			
		||||
    label: acc.name,
 | 
			
		||||
    value: acc.address,
 | 
			
		||||
  }));
 | 
			
		||||
  const [selectedAccount, setSelectedAccount] = useState<
 | 
			
		||||
    typeof accountOptions[0] | null
 | 
			
		||||
  >(null);
 | 
			
		||||
 | 
			
		||||
  const destAccountOptions = snap.accounts
 | 
			
		||||
    .map((acc) => ({
 | 
			
		||||
      label: acc.name,
 | 
			
		||||
      value: acc.address,
 | 
			
		||||
    }))
 | 
			
		||||
    .filter((acc) => acc.value !== selectedAccount?.value);
 | 
			
		||||
  const [selectedDestAccount, setSelectedDestAccount] = useState<
 | 
			
		||||
    typeof destAccountOptions[0] | null
 | 
			
		||||
  >(null);
 | 
			
		||||
 | 
			
		||||
  const [txIsLoading, setTxIsLoading] = useState(false);
 | 
			
		||||
  const [txIsDisabled, setTxIsDisabled] = useState(false);
 | 
			
		||||
  const [txFields, setTxFields] = useState<TxFields>({});
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const transactionType = selectedTransaction?.value;
 | 
			
		||||
    const account = snap.accounts.find(
 | 
			
		||||
      (acc) => acc.address === selectedAccount?.value
 | 
			
		||||
    );
 | 
			
		||||
    if (!account || !transactionType || txIsLoading) {
 | 
			
		||||
      setTxIsDisabled(true);
 | 
			
		||||
    } else {
 | 
			
		||||
      setTxIsDisabled(false);
 | 
			
		||||
    }
 | 
			
		||||
  }, [txIsLoading, selectedTransaction, selectedAccount, snap.accounts]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    let _txFields: TxFields | undefined = transactionsData.find(
 | 
			
		||||
      (tx) => tx.TransactionType === selectedTransaction?.value
 | 
			
		||||
    );
 | 
			
		||||
    if (!_txFields) return setTxFields({});
 | 
			
		||||
    _txFields = { ..._txFields } as TxFields;
 | 
			
		||||
 | 
			
		||||
    setSelectedDestAccount(null);
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    delete _txFields.TransactionType;
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    delete _txFields.Account;
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    delete _txFields.Sequence;
 | 
			
		||||
    setTxFields(_txFields);
 | 
			
		||||
  }, [selectedTransaction, setSelectedDestAccount]);
 | 
			
		||||
 | 
			
		||||
  const submitTest = useCallback(async () => {
 | 
			
		||||
    const account = snap.accounts.find(
 | 
			
		||||
      (acc) => acc.address === selectedAccount?.value
 | 
			
		||||
    );
 | 
			
		||||
    const TransactionType = selectedTransaction?.value;
 | 
			
		||||
    if (!account || !TransactionType || txIsDisabled) return;
 | 
			
		||||
 | 
			
		||||
    setTxIsLoading(true);
 | 
			
		||||
    // setTxIsError(null)
 | 
			
		||||
    try {
 | 
			
		||||
      let options = { ...txFields };
 | 
			
		||||
 | 
			
		||||
      options.Destination = selectedDestAccount?.value;
 | 
			
		||||
      (Object.keys(options) as (keyof TxFields)[]).forEach((field) => {
 | 
			
		||||
        let _value = options[field];
 | 
			
		||||
        // convert currency
 | 
			
		||||
        if (typeof _value === "object" && _value.type === "currency") {
 | 
			
		||||
          if (+_value.value) {
 | 
			
		||||
            options[field] = (+_value.value * 1000000 + "") as any;
 | 
			
		||||
          } else {
 | 
			
		||||
            options[field] = undefined; // 👇 💀
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        // handle type: `json`
 | 
			
		||||
        if (typeof _value === "object" && _value.type === "json") {
 | 
			
		||||
          if (typeof _value.value === "object") {
 | 
			
		||||
            options[field] = _value.value as any;
 | 
			
		||||
          } else {
 | 
			
		||||
            try {
 | 
			
		||||
              options[field] = JSON.parse(_value.value);
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
              const message = `Input error for json field '${field}': ${
 | 
			
		||||
                error instanceof Error ? error.message : ""
 | 
			
		||||
              }`;
 | 
			
		||||
              throw Error(message);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // delete unneccesary fields
 | 
			
		||||
        if (!options[field]) {
 | 
			
		||||
          delete options[field];
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      const logPrefix = header ? `${header.split(".")[0]}: ` : undefined;
 | 
			
		||||
      await sendTransaction(
 | 
			
		||||
        account,
 | 
			
		||||
        {
 | 
			
		||||
          TransactionType,
 | 
			
		||||
          ...options,
 | 
			
		||||
        },
 | 
			
		||||
        { logPrefix }
 | 
			
		||||
      );
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(error);
 | 
			
		||||
      if (error instanceof Error) {
 | 
			
		||||
        state.transactionLogs.push({ type: "error", message: error.message });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    setTxIsLoading(false);
 | 
			
		||||
  }, [
 | 
			
		||||
    header,
 | 
			
		||||
    selectedAccount?.value,
 | 
			
		||||
    selectedDestAccount?.value,
 | 
			
		||||
    selectedTransaction?.value,
 | 
			
		||||
    snap.accounts,
 | 
			
		||||
    txFields,
 | 
			
		||||
    txIsDisabled,
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
  const resetState = useCallback(() => {
 | 
			
		||||
    setSelectedAccount(null);
 | 
			
		||||
    setSelectedDestAccount(null);
 | 
			
		||||
    setSelectedTransaction(null);
 | 
			
		||||
    setTxFields({});
 | 
			
		||||
    setTxIsDisabled(false);
 | 
			
		||||
    setTxIsLoading(false);
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const usualFields = ["TransactionType", "Amount", "Account", "Destination"];
 | 
			
		||||
  const otherFields = Object.keys(txFields).filter(
 | 
			
		||||
    (k) => !usualFields.includes(k)
 | 
			
		||||
  ) as OtherFields;
 | 
			
		||||
  return (
 | 
			
		||||
    <Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
 | 
			
		||||
      <Container
 | 
			
		||||
        css={{ p: "$3 0", fontSize: "$sm", height: "calc(100% - 28px)" }}
 | 
			
		||||
      >
 | 
			
		||||
        <Flex column fluid css={{ height: "100%", overflowY: "auto" }}>
 | 
			
		||||
          <Flex
 | 
			
		||||
            row
 | 
			
		||||
            fluid
 | 
			
		||||
            css={{ justifyContent: "flex-end", alignItems: "center", mb: "$3" }}
 | 
			
		||||
          >
 | 
			
		||||
            <Text muted css={{ mr: "$3" }}>
 | 
			
		||||
              Transaction type:{" "}
 | 
			
		||||
            </Text>
 | 
			
		||||
            <Select
 | 
			
		||||
              instanceId="transactionsType"
 | 
			
		||||
              placeholder="Select transaction type"
 | 
			
		||||
              options={transactionsOptions}
 | 
			
		||||
              hideSelectedOptions
 | 
			
		||||
              css={{ width: "70%" }}
 | 
			
		||||
              value={selectedTransaction}
 | 
			
		||||
              onChange={(tt) => setSelectedTransaction(tt as any)}
 | 
			
		||||
            />
 | 
			
		||||
          </Flex>
 | 
			
		||||
          <Flex
 | 
			
		||||
            row
 | 
			
		||||
            fluid
 | 
			
		||||
            css={{ justifyContent: "flex-end", alignItems: "center", mb: "$3" }}
 | 
			
		||||
          >
 | 
			
		||||
            <Text muted css={{ mr: "$3" }}>
 | 
			
		||||
              Account:{" "}
 | 
			
		||||
            </Text>
 | 
			
		||||
            <Select
 | 
			
		||||
              instanceId="from-account"
 | 
			
		||||
              placeholder="Select your account"
 | 
			
		||||
              css={{ width: "70%" }}
 | 
			
		||||
              options={accountOptions}
 | 
			
		||||
              value={selectedAccount}
 | 
			
		||||
              onChange={(acc) => setSelectedAccount(acc as any)}
 | 
			
		||||
            />
 | 
			
		||||
          </Flex>
 | 
			
		||||
          {txFields.Amount !== undefined && (
 | 
			
		||||
            <Flex
 | 
			
		||||
              row
 | 
			
		||||
              fluid
 | 
			
		||||
              css={{
 | 
			
		||||
                justifyContent: "flex-end",
 | 
			
		||||
                alignItems: "center",
 | 
			
		||||
                mb: "$3",
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Text muted css={{ mr: "$3" }}>
 | 
			
		||||
                Amount (XRP):{" "}
 | 
			
		||||
              </Text>
 | 
			
		||||
              <Input
 | 
			
		||||
                value={txFields.Amount.value}
 | 
			
		||||
                onChange={(e) =>
 | 
			
		||||
                  setTxFields({
 | 
			
		||||
                    ...txFields,
 | 
			
		||||
                    Amount: { type: "currency", value: e.target.value },
 | 
			
		||||
                  })
 | 
			
		||||
                }
 | 
			
		||||
                variant="deep"
 | 
			
		||||
                css={{ width: "70%", flex: "inherit", height: "$9" }}
 | 
			
		||||
              />
 | 
			
		||||
            </Flex>
 | 
			
		||||
          )}
 | 
			
		||||
          {txFields.Destination !== undefined && (
 | 
			
		||||
            <Flex
 | 
			
		||||
              row
 | 
			
		||||
              fluid
 | 
			
		||||
              css={{
 | 
			
		||||
                justifyContent: "flex-end",
 | 
			
		||||
                alignItems: "center",
 | 
			
		||||
                mb: "$3",
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Text muted css={{ mr: "$3" }}>
 | 
			
		||||
                Destination account:{" "}
 | 
			
		||||
              </Text>
 | 
			
		||||
              <Select
 | 
			
		||||
                instanceId="to-account"
 | 
			
		||||
                placeholder="Select the destination account"
 | 
			
		||||
                css={{ width: "70%" }}
 | 
			
		||||
                options={destAccountOptions}
 | 
			
		||||
                value={selectedDestAccount}
 | 
			
		||||
                isClearable
 | 
			
		||||
                onChange={(acc) => setSelectedDestAccount(acc as any)}
 | 
			
		||||
              />
 | 
			
		||||
            </Flex>
 | 
			
		||||
          )}
 | 
			
		||||
          {otherFields.map((field) => {
 | 
			
		||||
            let _value = txFields[field];
 | 
			
		||||
            let value = typeof _value === "object" ? _value.value : _value;
 | 
			
		||||
            value =
 | 
			
		||||
              typeof value === "object"
 | 
			
		||||
                ? JSON.stringify(value)
 | 
			
		||||
                : value?.toLocaleString();
 | 
			
		||||
            let isCurrency =
 | 
			
		||||
              typeof _value === "object" && _value.type === "currency";
 | 
			
		||||
            return (
 | 
			
		||||
              <Flex
 | 
			
		||||
                key={field}
 | 
			
		||||
                row
 | 
			
		||||
                fluid
 | 
			
		||||
                css={{
 | 
			
		||||
                  justifyContent: "flex-end",
 | 
			
		||||
                  alignItems: "center",
 | 
			
		||||
                  mb: "$3",
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Text muted css={{ mr: "$3" }}>
 | 
			
		||||
                  {field + (isCurrency ? " (XRP)" : "")}:{" "}
 | 
			
		||||
                </Text>
 | 
			
		||||
                <Input
 | 
			
		||||
                  value={value}
 | 
			
		||||
                  onChange={(e) =>
 | 
			
		||||
                    setTxFields({
 | 
			
		||||
                      ...txFields,
 | 
			
		||||
                      [field]:
 | 
			
		||||
                        typeof _value === "object"
 | 
			
		||||
                          ? { ..._value, value: e.target.value }
 | 
			
		||||
                          : e.target.value,
 | 
			
		||||
                    })
 | 
			
		||||
                  }
 | 
			
		||||
                  variant="deep"
 | 
			
		||||
                  css={{ width: "70%", flex: "inherit", height: "$9" }}
 | 
			
		||||
                />
 | 
			
		||||
              </Flex>
 | 
			
		||||
            );
 | 
			
		||||
          })}
 | 
			
		||||
        </Flex>
 | 
			
		||||
      </Container>
 | 
			
		||||
      <Flex
 | 
			
		||||
        row
 | 
			
		||||
        css={{
 | 
			
		||||
          justifyContent: "space-between",
 | 
			
		||||
          position: "absolute",
 | 
			
		||||
          left: 0,
 | 
			
		||||
          bottom: 0,
 | 
			
		||||
          width: "100%",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Button outline>VIEW AS JSON</Button>
 | 
			
		||||
        <Flex row>
 | 
			
		||||
          <Button onClick={resetState} outline css={{ mr: "$3" }}>
 | 
			
		||||
            RESET
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button
 | 
			
		||||
            variant="primary"
 | 
			
		||||
            onClick={submitTest}
 | 
			
		||||
            isLoading={txIsLoading}
 | 
			
		||||
            disabled={txIsDisabled}
 | 
			
		||||
          >
 | 
			
		||||
            <Play weight="bold" size="16px" />
 | 
			
		||||
            RUN TEST
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Flex>
 | 
			
		||||
      </Flex>
 | 
			
		||||
    </Box>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const Test = () => {
 | 
			
		||||
  // This and useEffect is here to prevent useLayoutEffect warnings from react-split
 | 
			
		||||
  const [showComponent, setShowComponent] = useState(false);
 | 
			
		||||
  const { transactionLogs } = useSnapshot(state);
 | 
			
		||||
  const [tabHeaders, setTabHeaders] = useState<string[]>(["test1.json"]);
 | 
			
		||||
  const { transactions, activeHeader } = useSnapshot(transactionsState);
 | 
			
		||||
  const snap = useSnapshot(state);
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setShowComponent(true);
 | 
			
		||||
  }, []);
 | 
			
		||||
  if (!showComponent) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  const hasScripts = Boolean(
 | 
			
		||||
    snap.files.filter(f => f.name.toLowerCase()?.endsWith(".js")).length
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const renderNav = () => (
 | 
			
		||||
    <Flex css={{ gap: "$3" }}>
 | 
			
		||||
      {snap.files
 | 
			
		||||
        .filter(f => f.name.endsWith(".js"))
 | 
			
		||||
        .map(file => (
 | 
			
		||||
          <RunScript file={file} key={file.name} />
 | 
			
		||||
        ))}
 | 
			
		||||
    </Flex>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Container css={{ px: 0 }}>
 | 
			
		||||
      <Split
 | 
			
		||||
        direction="vertical"
 | 
			
		||||
        sizes={getSplit("testVertical") || [50, 50]}
 | 
			
		||||
        sizes={
 | 
			
		||||
          hasScripts && getSplit("testVertical")?.length === 2
 | 
			
		||||
            ? [50, 20, 30]
 | 
			
		||||
            : hasScripts
 | 
			
		||||
            ? [50, 20, 50]
 | 
			
		||||
            : [50, 50]
 | 
			
		||||
        }
 | 
			
		||||
        gutterSize={4}
 | 
			
		||||
        gutterAlign="center"
 | 
			
		||||
        style={{ height: "calc(100vh - 60px)" }}
 | 
			
		||||
        onDragEnd={(e) => saveSplit("testVertical", e)}
 | 
			
		||||
        onDragEnd={e => saveSplit("testVertical", e)}
 | 
			
		||||
      >
 | 
			
		||||
        <Flex
 | 
			
		||||
          row
 | 
			
		||||
@@ -376,23 +83,30 @@ const Test = () => {
 | 
			
		||||
              width: "100%",
 | 
			
		||||
              height: "100%",
 | 
			
		||||
            }}
 | 
			
		||||
            onDragEnd={(e) => saveSplit("testHorizontal", e)}
 | 
			
		||||
            onDragEnd={e => saveSplit("testHorizontal", e)}
 | 
			
		||||
          >
 | 
			
		||||
            <Box css={{ width: "55%", px: "$2" }}>
 | 
			
		||||
              <Tabs
 | 
			
		||||
                label="Transaction"
 | 
			
		||||
                activeHeader={activeHeader}
 | 
			
		||||
                // TODO make header a required field
 | 
			
		||||
                onChangeActive={(idx, header) => {
 | 
			
		||||
                  if (header) transactionsState.activeHeader = header;
 | 
			
		||||
                }}
 | 
			
		||||
                keepAllAlive
 | 
			
		||||
                forceDefaultExtension
 | 
			
		||||
                defaultExtension=".json"
 | 
			
		||||
                onCreateNewTab={(name) =>
 | 
			
		||||
                  setTabHeaders(tabHeaders.concat(name))
 | 
			
		||||
                defaultExtension="json"
 | 
			
		||||
                allowedExtensions={["json"]}
 | 
			
		||||
                onCreateNewTab={header => modifyTxState(header, {})}
 | 
			
		||||
                onRenameTab={(idx, nwName, oldName = "") =>
 | 
			
		||||
                  renameTxState(oldName, nwName)
 | 
			
		||||
                }
 | 
			
		||||
                onCloseTab={(index) =>
 | 
			
		||||
                  setTabHeaders(tabHeaders.filter((_, idx) => idx !== index))
 | 
			
		||||
                onCloseTab={(idx, header) =>
 | 
			
		||||
                  header && modifyTxState(header, undefined)
 | 
			
		||||
                }
 | 
			
		||||
              >
 | 
			
		||||
                {tabHeaders.map((header) => (
 | 
			
		||||
                {transactions.map(({ header, state }) => (
 | 
			
		||||
                  <Tab key={header} header={header}>
 | 
			
		||||
                    <Transaction header={header} />
 | 
			
		||||
                    <Transaction state={state} header={header} />
 | 
			
		||||
                  </Tab>
 | 
			
		||||
                ))}
 | 
			
		||||
              </Tabs>
 | 
			
		||||
@@ -402,8 +116,25 @@ const Test = () => {
 | 
			
		||||
            </Box>
 | 
			
		||||
          </Split>
 | 
			
		||||
        </Flex>
 | 
			
		||||
 | 
			
		||||
        <Flex row fluid>
 | 
			
		||||
        {hasScripts ? (
 | 
			
		||||
          <Flex
 | 
			
		||||
            as="div"
 | 
			
		||||
            css={{
 | 
			
		||||
              borderTop: "1px solid $mauve6",
 | 
			
		||||
              background: "$mauve1",
 | 
			
		||||
              flexDirection: "column",
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <LogBox
 | 
			
		||||
              Icon={FileJs}
 | 
			
		||||
              title="Helper scripts"
 | 
			
		||||
              logs={snap.scriptLogs}
 | 
			
		||||
              clearLog={() => (state.scriptLogs = [])}
 | 
			
		||||
              renderNav={renderNav}
 | 
			
		||||
            />
 | 
			
		||||
          </Flex>
 | 
			
		||||
        ) : null}
 | 
			
		||||
        <Flex>
 | 
			
		||||
          <Split
 | 
			
		||||
            direction="horizontal"
 | 
			
		||||
            sizes={[50, 50]}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,419 +0,0 @@
 | 
			
		||||
diff --git a/node_modules/ripple-binary-codec/dist/enums/definitions.json b/node_modules/ripple-binary-codec/dist/enums/definitions.json
 | 
			
		||||
index 2333c42..b8f8eab 100644
 | 
			
		||||
--- a/node_modules/ripple-binary-codec/dist/enums/definitions.json
 | 
			
		||||
+++ b/node_modules/ripple-binary-codec/dist/enums/definitions.json
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
+
 | 
			
		||||
 {
 | 
			
		||||
   "TYPES": {
 | 
			
		||||
     "Validation": 10003,
 | 
			
		||||
@@ -40,9 +41,7 @@
 | 
			
		||||
     "Check": 67,
 | 
			
		||||
     "Nickname": 110,
 | 
			
		||||
     "Contract": 99,
 | 
			
		||||
-    "NFTokenPage": 80,
 | 
			
		||||
-    "NFTokenOffer": 55,
 | 
			
		||||
-    "NegativeUNL": 78
 | 
			
		||||
+    "GeneratorMap": 103
 | 
			
		||||
   },
 | 
			
		||||
   "FIELDS": [
 | 
			
		||||
     [
 | 
			
		||||
@@ -95,16 +94,6 @@
 | 
			
		||||
         "type": "UInt16"
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "TransferFee",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 4,
 | 
			
		||||
-        "isVLEncoded": false,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "UInt16"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
     [
 | 
			
		||||
       "Flags",
 | 
			
		||||
       {
 | 
			
		||||
@@ -455,6 +444,16 @@
 | 
			
		||||
         "type": "UInt32"
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
+    [
 | 
			
		||||
+      "EmitGeneration",
 | 
			
		||||
+      {
 | 
			
		||||
+        "nth": 43,
 | 
			
		||||
+        "isVLEncoded": false,
 | 
			
		||||
+        "isSerialized": true,
 | 
			
		||||
+        "isSigningField": true,
 | 
			
		||||
+        "type": "UInt32"
 | 
			
		||||
+      }
 | 
			
		||||
+    ],
 | 
			
		||||
     [
 | 
			
		||||
       "IndexNext",
 | 
			
		||||
       {
 | 
			
		||||
@@ -635,16 +634,6 @@
 | 
			
		||||
         "type": "Hash256"
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "TokenID",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 10,
 | 
			
		||||
-        "isVLEncoded": false,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "Hash256"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
     [
 | 
			
		||||
       "BookDirectory",
 | 
			
		||||
       {
 | 
			
		||||
@@ -916,7 +905,7 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "URI",
 | 
			
		||||
+      "Generator",
 | 
			
		||||
       {
 | 
			
		||||
         "nth": 5,
 | 
			
		||||
         "isVLEncoded": true,
 | 
			
		||||
@@ -1045,36 +1034,6 @@
 | 
			
		||||
         "type": "Blob"
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "UNLModifyValidator",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 19,
 | 
			
		||||
-        "isVLEncoded": true,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "Blob"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "ValidatorToDisable",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 20,
 | 
			
		||||
-        "isVLEncoded": true,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "Blob"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "ValidatorToReEnable",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 21,
 | 
			
		||||
-        "isVLEncoded": true,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "Blob"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
     [
 | 
			
		||||
       "Account",
 | 
			
		||||
       {
 | 
			
		||||
@@ -1156,7 +1115,7 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "Minter",
 | 
			
		||||
+      "EmitCallback",
 | 
			
		||||
       {
 | 
			
		||||
         "nth": 9,
 | 
			
		||||
         "isVLEncoded": true,
 | 
			
		||||
@@ -1276,9 +1235,9 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "NonFungibleToken",
 | 
			
		||||
+      "Signer",
 | 
			
		||||
       {
 | 
			
		||||
-        "nth": 12,
 | 
			
		||||
+        "nth": 16,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
         "isSerialized": true,
 | 
			
		||||
         "isSigningField": true,
 | 
			
		||||
@@ -1286,9 +1245,9 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "Signer",
 | 
			
		||||
+      "Majority",
 | 
			
		||||
       {
 | 
			
		||||
-        "nth": 16,
 | 
			
		||||
+        "nth": 18,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
         "isSerialized": true,
 | 
			
		||||
         "isSigningField": true,
 | 
			
		||||
@@ -1296,9 +1255,9 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "Majority",
 | 
			
		||||
+      "DisabledValidator",
 | 
			
		||||
       {
 | 
			
		||||
-        "nth": 18,
 | 
			
		||||
+        "nth": 19,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
         "isSerialized": true,
 | 
			
		||||
         "isSigningField": true,
 | 
			
		||||
@@ -1306,9 +1265,9 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "DisabledValidator",
 | 
			
		||||
+      "EmitDetails",
 | 
			
		||||
       {
 | 
			
		||||
-        "nth": 19,
 | 
			
		||||
+        "nth": 12,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
         "isSerialized": true,
 | 
			
		||||
         "isSigningField": true,
 | 
			
		||||
@@ -1395,16 +1354,6 @@
 | 
			
		||||
         "type": "STArray"
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "NonFungibleTokens",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 10,
 | 
			
		||||
-        "isVLEncoded": false,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "STArray"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
     [
 | 
			
		||||
       "Majorities",
 | 
			
		||||
       {
 | 
			
		||||
@@ -1415,16 +1364,6 @@
 | 
			
		||||
         "type": "STArray"
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "DisabledValidators",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 17,
 | 
			
		||||
-        "isVLEncoded": false,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "STArray"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
     [
 | 
			
		||||
       "CloseResolution",
 | 
			
		||||
       {
 | 
			
		||||
@@ -1535,16 +1474,6 @@
 | 
			
		||||
         "type": "Vector256"
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "TokenIDs",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 4,
 | 
			
		||||
-        "isVLEncoded": true,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "Vector256"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
     [
 | 
			
		||||
       "Transaction",
 | 
			
		||||
       {
 | 
			
		||||
@@ -1596,7 +1525,7 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "TicketCount",
 | 
			
		||||
+      "HookStateCount",
 | 
			
		||||
       {
 | 
			
		||||
         "nth": 40,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
@@ -1606,7 +1535,7 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "TicketSequence",
 | 
			
		||||
+      "HookReserveCount",
 | 
			
		||||
       {
 | 
			
		||||
         "nth": 41,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
@@ -1616,7 +1545,7 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "TokenTaxon",
 | 
			
		||||
+      "HookDataMaxSize",
 | 
			
		||||
       {
 | 
			
		||||
         "nth": 42,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
@@ -1626,23 +1555,23 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "MintedTokens",
 | 
			
		||||
+      "HookOn",
 | 
			
		||||
       {
 | 
			
		||||
-        "nth": 43,
 | 
			
		||||
+        "nth": 16,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
         "isSerialized": true,
 | 
			
		||||
         "isSigningField": true,
 | 
			
		||||
-        "type": "UInt32"
 | 
			
		||||
+        "type": "UInt64"
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "BurnedTokens",
 | 
			
		||||
+      "EmitBurden",
 | 
			
		||||
       {
 | 
			
		||||
-        "nth": 44,
 | 
			
		||||
+        "nth": 12,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
         "isSerialized": true,
 | 
			
		||||
         "isSigningField": true,
 | 
			
		||||
-        "type": "UInt32"
 | 
			
		||||
+        "type": "UInt64"
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
@@ -1686,29 +1615,9 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "PreviousPageMin",
 | 
			
		||||
+      "EmitParentTxnID",
 | 
			
		||||
       {
 | 
			
		||||
-        "nth": 26,
 | 
			
		||||
-        "isVLEncoded": false,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "Hash256"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "NextPageMin",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 27,
 | 
			
		||||
-        "isVLEncoded": false,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "Hash256"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "BuyOffer",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 28,
 | 
			
		||||
+        "nth": 10,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
         "isSerialized": true,
 | 
			
		||||
         "isSigningField": true,
 | 
			
		||||
@@ -1716,9 +1625,9 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "SellOffer",
 | 
			
		||||
+      "EmitNonce",
 | 
			
		||||
       {
 | 
			
		||||
-        "nth": 29,
 | 
			
		||||
+        "nth": 11,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
         "isSerialized": true,
 | 
			
		||||
         "isSigningField": true,
 | 
			
		||||
@@ -1735,16 +1644,6 @@
 | 
			
		||||
         "type": "UInt8"
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "UNLModifyDisabling",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 17,
 | 
			
		||||
-        "isVLEncoded": false,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "UInt8"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
     [
 | 
			
		||||
       "DestinationNode",
 | 
			
		||||
       {
 | 
			
		||||
@@ -1754,36 +1653,6 @@
 | 
			
		||||
         "isSigningField": true,
 | 
			
		||||
         "type": "UInt64"
 | 
			
		||||
       }
 | 
			
		||||
-    ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "Cookie",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 10,
 | 
			
		||||
-        "isVLEncoded": false,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "UInt64"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "ServerVersion",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 11,
 | 
			
		||||
-        "isVLEncoded": false,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "UInt64"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "OfferNode",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 12,
 | 
			
		||||
-        "isVLEncoded": false,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "UInt64"
 | 
			
		||||
-      }
 | 
			
		||||
     ]
 | 
			
		||||
   ],
 | 
			
		||||
   "TRANSACTION_RESULTS": {
 | 
			
		||||
@@ -1908,18 +1777,7 @@
 | 
			
		||||
     "tecDUPLICATE": 149,
 | 
			
		||||
     "tecKILLED": 150,
 | 
			
		||||
     "tecHAS_OBLIGATIONS": 151,
 | 
			
		||||
-    "tecTOO_SOON": 152,
 | 
			
		||||
-
 | 
			
		||||
-    "tecMAX_SEQUENCE_REACHED": 154,
 | 
			
		||||
-    "tecNO_SUITABLE_PAGE": 155,
 | 
			
		||||
-    "tecBUY_SELL_MISMATCH": 156,
 | 
			
		||||
-    "tecOFFER_TYPE_MISMATCH": 157,
 | 
			
		||||
-    "tecCANT_ACCEPT_OWN_OFFER": 158,
 | 
			
		||||
-    "tecINSUFFICIENT_FUNDS": 159,
 | 
			
		||||
-    "tecOBJECT_NOT_FOUND": 160,
 | 
			
		||||
-    "tecINSUFFICIENT_PAYMENT": 161,
 | 
			
		||||
-    "tecINCORRECT_ASSET": 162,
 | 
			
		||||
-    "tecTOO_MANY": 163
 | 
			
		||||
+    "tecTOO_SOON": 152
 | 
			
		||||
   },
 | 
			
		||||
   "TRANSACTION_TYPES": {
 | 
			
		||||
     "Invalid": -1,
 | 
			
		||||
@@ -1946,13 +1804,11 @@
 | 
			
		||||
     "DepositPreauth": 19,
 | 
			
		||||
     "TrustSet": 20,
 | 
			
		||||
     "AccountDelete": 21,
 | 
			
		||||
-    "NFTokenMint": 25,
 | 
			
		||||
-    "NFTokenBurn": 26,
 | 
			
		||||
-    "NFTokenCreateOffer": 27,
 | 
			
		||||
-    "NFTokenCancelOffer": 28,
 | 
			
		||||
-    "NFTokenAcceptOffer": 29,
 | 
			
		||||
+    "SetHook": 22,
 | 
			
		||||
+    "Invoke": 23,
 | 
			
		||||
+    "Batch": 24,
 | 
			
		||||
+
 | 
			
		||||
     "EnableAmendment": 100,
 | 
			
		||||
-    "SetFee": 101,
 | 
			
		||||
-    "UNLModify": 102
 | 
			
		||||
+    "SetFee": 101
 | 
			
		||||
   }
 | 
			
		||||
 }
 | 
			
		||||
							
								
								
									
										1827
									
								
								patches/ripple-binary-codec+1.4.2.patch
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1827
									
								
								patches/ripple-binary-codec+1.4.2.patch
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -27,50 +27,64 @@ export const names = [
 | 
			
		||||
 * new account with 10 000 XRP. Hooks Testnet /newcreds endpoint
 | 
			
		||||
 * is protected with CORS so that's why we did our own endpoint
 | 
			
		||||
 */
 | 
			
		||||
export const addFaucetAccount = async (showToast: boolean = false) => {
 | 
			
		||||
  // Lets limit the number of faucet accounts to 5 for now
 | 
			
		||||
  if (state.accounts.length > 4) {
 | 
			
		||||
    return toast.error("You can only have maximum 5 accounts");
 | 
			
		||||
  }
 | 
			
		||||
  if (typeof window !== 'undefined') {
 | 
			
		||||
export const addFaucetAccount = async (name?: string, showToast: boolean = false) => {
 | 
			
		||||
  if (typeof window === undefined) return
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    const toastId = showToast ? toast.loading("Creating account") : "";
 | 
			
		||||
    const res = await fetch(`${window.location.origin}/api/faucet`, {
 | 
			
		||||
      method: "POST",
 | 
			
		||||
    });
 | 
			
		||||
    const json: FaucetAccountRes | { error: string } = await res.json();
 | 
			
		||||
    if ("error" in json) {
 | 
			
		||||
      if (showToast) {
 | 
			
		||||
        return toast.error(json.error, { id: toastId });
 | 
			
		||||
      } else {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
  const toastId = showToast ? toast.loading("Creating account") : "";
 | 
			
		||||
  const res = await fetch(`${window.location.origin}/api/faucet`, {
 | 
			
		||||
    method: "POST",
 | 
			
		||||
  });
 | 
			
		||||
  const json: FaucetAccountRes | { error: string } = await res.json();
 | 
			
		||||
  if ("error" in json) {
 | 
			
		||||
    if (showToast) {
 | 
			
		||||
      return toast.error(json.error, { id: toastId });
 | 
			
		||||
    } else {
 | 
			
		||||
      if (showToast) {
 | 
			
		||||
        toast.success("New account created", { id: toastId });
 | 
			
		||||
      }
 | 
			
		||||
      state.accounts.push({
 | 
			
		||||
        name: names[state.accounts.length],
 | 
			
		||||
        xrp: (json.xrp || 0 * 1000000).toString(),
 | 
			
		||||
        address: json.address,
 | 
			
		||||
        secret: json.secret,
 | 
			
		||||
        sequence: 1,
 | 
			
		||||
        hooks: [],
 | 
			
		||||
        isLoading: false,
 | 
			
		||||
      });
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    if (showToast) {
 | 
			
		||||
      toast.success("New account created", { id: toastId });
 | 
			
		||||
    }
 | 
			
		||||
    const currNames = state.accounts.map(acc => acc.name);
 | 
			
		||||
    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: 1,
 | 
			
		||||
      hooks: [],
 | 
			
		||||
      isLoading: false,
 | 
			
		||||
      version: '2'
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// fetch initial faucets
 | 
			
		||||
(async function fetchFaucets() {
 | 
			
		||||
  if (typeof window !== 'undefined') {
 | 
			
		||||
    if (state.accounts.length < 2) {
 | 
			
		||||
    if (state.accounts.length === 0) {
 | 
			
		||||
      await addFaucetAccount();
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        addFaucetAccount();
 | 
			
		||||
      }, 10000);
 | 
			
		||||
      // setTimeout(() => {
 | 
			
		||||
      //   addFaucetAccount();
 | 
			
		||||
      // }, 10000);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
})();
 | 
			
		||||
})();
 | 
			
		||||
 | 
			
		||||
export const addFunds = async (address: string) => {
 | 
			
		||||
  const toastId = toast.loading("Requesting funds");
 | 
			
		||||
  const res = await fetch(`${window.location.origin}/api/faucet?account=${address}`, {
 | 
			
		||||
    method: "POST",
 | 
			
		||||
  });
 | 
			
		||||
  const json: FaucetAccountRes | { error: string } = await res.json();
 | 
			
		||||
  if ("error" in json) {
 | 
			
		||||
    return toast.error(json.error, { id: toastId });
 | 
			
		||||
  } else {
 | 
			
		||||
    toast.success(`Funds added (${json.xrp} XRP)`, { id: toastId });
 | 
			
		||||
    const currAccount = state.accounts.find(acc => acc.address === address);
 | 
			
		||||
    if (currAccount) {
 | 
			
		||||
      currAccount.xrp = (Number(currAccount.xrp) + (json.xrp * 1000000)).toString();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -14,78 +14,113 @@ import { ref } from "valtio";
 | 
			
		||||
 */
 | 
			
		||||
export const compileCode = async (activeId: number) => {
 | 
			
		||||
  // Save the file to global state
 | 
			
		||||
  saveFile(false);
 | 
			
		||||
  saveFile(false, activeId);
 | 
			
		||||
  if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) {
 | 
			
		||||
    throw Error("Missing env!");
 | 
			
		||||
  }
 | 
			
		||||
  // Bail out if we're already compiling
 | 
			
		||||
  if (state.compiling) {
 | 
			
		||||
    // if compiling is ongoing return
 | 
			
		||||
    // if compiling is ongoing return // TODO Inform user about it.
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  // Set loading state to true
 | 
			
		||||
  state.compiling = true;
 | 
			
		||||
  state.logs = []
 | 
			
		||||
  const file = state.files[activeId]
 | 
			
		||||
  try {
 | 
			
		||||
    const res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
 | 
			
		||||
      method: "POST",
 | 
			
		||||
      headers: {
 | 
			
		||||
        "Content-Type": "application/json",
 | 
			
		||||
      },
 | 
			
		||||
      body: JSON.stringify({
 | 
			
		||||
        output: "wasm",
 | 
			
		||||
        compress: true,
 | 
			
		||||
        files: [
 | 
			
		||||
          {
 | 
			
		||||
            type: "c",
 | 
			
		||||
            name: state.files[activeId].name,
 | 
			
		||||
            src: state.files[activeId].content,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      }),
 | 
			
		||||
    });
 | 
			
		||||
    file.containsErrors = false
 | 
			
		||||
    let res: Response
 | 
			
		||||
    try {
 | 
			
		||||
      res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
 | 
			
		||||
        method: "POST",
 | 
			
		||||
        headers: {
 | 
			
		||||
          "Content-Type": "application/json",
 | 
			
		||||
        },
 | 
			
		||||
        body: JSON.stringify({
 | 
			
		||||
          output: "wasm",
 | 
			
		||||
          compress: true,
 | 
			
		||||
          strip: state.compileOptions.strip,
 | 
			
		||||
          files: [
 | 
			
		||||
            {
 | 
			
		||||
              type: "c",
 | 
			
		||||
              options: state.compileOptions.optimizationLevel || '-O2',
 | 
			
		||||
              name: file.name,
 | 
			
		||||
              src: file.content,
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        }),
 | 
			
		||||
      });
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      throw Error("Something went wrong, check your network connection and try again!")
 | 
			
		||||
    }
 | 
			
		||||
    const json = await res.json();
 | 
			
		||||
    state.compiling = false;
 | 
			
		||||
    if (!json.success) {
 | 
			
		||||
      state.logs.push({ type: "error", message: json.message });
 | 
			
		||||
      const errors = [json.message]
 | 
			
		||||
      if (json.tasks && json.tasks.length > 0) {
 | 
			
		||||
        json.tasks.forEach((task: any) => {
 | 
			
		||||
          if (!task.success) {
 | 
			
		||||
            state.logs.push({ type: "error", message: task?.console });
 | 
			
		||||
            errors.push(task?.console)
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return toast.error(`Couldn't compile!`, { position: "bottom-center" });
 | 
			
		||||
      throw errors
 | 
			
		||||
    }
 | 
			
		||||
    state.logs.push({
 | 
			
		||||
      type: "success",
 | 
			
		||||
      message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`,
 | 
			
		||||
      link: Router.asPath.replace("develop", "deploy"),
 | 
			
		||||
      linkText: "Go to deploy",
 | 
			
		||||
    });
 | 
			
		||||
    // Decode base64 encoded wasm that is coming back from the endpoint
 | 
			
		||||
    const bufferData = await decodeBinary(json.output);
 | 
			
		||||
    state.files[state.active].compiledContent = ref(bufferData);
 | 
			
		||||
    state.files[state.active].lastCompiled = new Date();
 | 
			
		||||
    // Import wabt from and create human readable version of wasm file and
 | 
			
		||||
    // put it into state
 | 
			
		||||
    import("wabt").then((wabt) => {
 | 
			
		||||
      const ww = wabt.default();
 | 
			
		||||
    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 import('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" });
 | 
			
		||||
 | 
			
		||||
      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);
 | 
			
		||||
    state.logs.push({
 | 
			
		||||
      type: "error",
 | 
			
		||||
      message: "Error occured while compiling!",
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -14,4 +14,11 @@ export const createNewFile = (name: string) => {
 | 
			
		||||
  const emptyFile: IFile = { name, language: languageMapping[fileExt as 'ts' | 'js' | 'md' | 'c' | 'h' | 'other'], content: "" };
 | 
			
		||||
  state.files.push(emptyFile);
 | 
			
		||||
  state.active = state.files.length - 1;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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}`)
 | 
			
		||||
 | 
			
		||||
  file.name = nwName
 | 
			
		||||
};
 | 
			
		||||
@@ -1,98 +0,0 @@
 | 
			
		||||
import { derive, sign } from "xrpl-accountlib";
 | 
			
		||||
 | 
			
		||||
import state, { IAccount } from "../index";
 | 
			
		||||
 | 
			
		||||
function arrayBufferToHex(arrayBuffer?: ArrayBuffer | null) {
 | 
			
		||||
  if (!arrayBuffer) {
 | 
			
		||||
    return "";
 | 
			
		||||
  }
 | 
			
		||||
  if (
 | 
			
		||||
    typeof arrayBuffer !== "object" ||
 | 
			
		||||
    arrayBuffer === null ||
 | 
			
		||||
    typeof arrayBuffer.byteLength !== "number"
 | 
			
		||||
  ) {
 | 
			
		||||
    throw new TypeError("Expected input to be an ArrayBuffer");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  var view = new Uint8Array(arrayBuffer);
 | 
			
		||||
  var result = "";
 | 
			
		||||
  var value;
 | 
			
		||||
 | 
			
		||||
  for (var i = 0; i < view.length; i++) {
 | 
			
		||||
    value = view[i].toString(16);
 | 
			
		||||
    result += value.length === 1 ? "0" + value : value;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* deployHook function turns the wasm binary into
 | 
			
		||||
 * hex string, signs the transaction and deploys it to
 | 
			
		||||
 * Hooks testnet.
 | 
			
		||||
 */
 | 
			
		||||
export const deployHook = async (account: IAccount & { name?: string }) => {
 | 
			
		||||
  if (
 | 
			
		||||
    !state.files ||
 | 
			
		||||
    state.files.length === 0 ||
 | 
			
		||||
    !state.files?.[state.active]?.compiledContent
 | 
			
		||||
  ) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!state.files?.[state.active]?.compiledContent) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!state.client) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (typeof window !== "undefined") {
 | 
			
		||||
    const tx = {
 | 
			
		||||
      Account: account.address,
 | 
			
		||||
      TransactionType: "SetHook",
 | 
			
		||||
      CreateCode: arrayBufferToHex(
 | 
			
		||||
        state.files?.[state.active]?.compiledContent
 | 
			
		||||
      ).toUpperCase(),
 | 
			
		||||
      HookOn: "0000000000000000",
 | 
			
		||||
      Sequence: account.sequence,
 | 
			
		||||
      Fee: "1000",
 | 
			
		||||
    };
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const submitRes = await state.client.send({
 | 
			
		||||
        command: "submit",
 | 
			
		||||
        tx_blob: signedTransaction,
 | 
			
		||||
      });
 | 
			
		||||
      if (submitRes.engine_result === "tesSUCCESS") {
 | 
			
		||||
        state.deployLogs.push({
 | 
			
		||||
          type: "success",
 | 
			
		||||
          message: "Hook deployed successfully ✅",
 | 
			
		||||
        });
 | 
			
		||||
        state.deployLogs.push({
 | 
			
		||||
          type: "success",
 | 
			
		||||
          message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`,
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        state.deployLogs.push({
 | 
			
		||||
          type: "error",
 | 
			
		||||
          message: `[${submitRes.engine_result}] ${submitRes.engine_result_message}`,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      console.log(err);
 | 
			
		||||
      state.deployLogs.push({
 | 
			
		||||
        type: "error",
 | 
			
		||||
        message: "Error occured while deploying",
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    if (currentAccount) {
 | 
			
		||||
      currentAccount.isLoading = false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										286
									
								
								state/actions/deployHook.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										286
									
								
								state/actions/deployHook.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,286 @@
 | 
			
		||||
import { derive, sign } from "xrpl-accountlib";
 | 
			
		||||
import toast from "react-hot-toast";
 | 
			
		||||
 | 
			
		||||
import state, { IAccount } from "../index";
 | 
			
		||||
import calculateHookOn, { TTS } from "../../utils/hookOnCalculator";
 | 
			
		||||
import { Link } from "../../components";
 | 
			
		||||
import { ref } from "valtio";
 | 
			
		||||
import estimateFee from "../../utils/estimateFee";
 | 
			
		||||
import { SetHookData } from '../../utils/setHook';
 | 
			
		||||
 | 
			
		||||
export const sha256 = async (string: string) => {
 | 
			
		||||
  const utf8 = new TextEncoder().encode(string);
 | 
			
		||||
  const hashBuffer = await crypto.subtle.digest("SHA-256", utf8);
 | 
			
		||||
  const hashArray = Array.from(new Uint8Array(hashBuffer));
 | 
			
		||||
  const hashHex = hashArray
 | 
			
		||||
    .map((bytes) => bytes.toString(16).padStart(2, "0"))
 | 
			
		||||
    .join("");
 | 
			
		||||
  return hashHex;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function toHex(str: string) {
 | 
			
		||||
  var result = "";
 | 
			
		||||
  for (var i = 0; i < str.length; i++) {
 | 
			
		||||
    result += str.charCodeAt(i).toString(16);
 | 
			
		||||
  }
 | 
			
		||||
  return result.toUpperCase();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function arrayBufferToHex(arrayBuffer?: ArrayBuffer | null) {
 | 
			
		||||
  if (!arrayBuffer) {
 | 
			
		||||
    return "";
 | 
			
		||||
  }
 | 
			
		||||
  if (
 | 
			
		||||
    typeof arrayBuffer !== "object" ||
 | 
			
		||||
    arrayBuffer === null ||
 | 
			
		||||
    typeof arrayBuffer.byteLength !== "number"
 | 
			
		||||
  ) {
 | 
			
		||||
    throw new TypeError("Expected input to be an ArrayBuffer");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  var view = new Uint8Array(arrayBuffer);
 | 
			
		||||
  var result = "";
 | 
			
		||||
  var value;
 | 
			
		||||
 | 
			
		||||
  for (var i = 0; i < view.length; i++) {
 | 
			
		||||
    value = view[i].toString(16);
 | 
			
		||||
    result += value.length === 1 ? "0" + value : value;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const prepareDeployHookTx = 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];
 | 
			
		||||
 | 
			
		||||
  if (!state.files || state.files.length === 0) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!activeFile?.compiledContent) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!state.client) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  const HookNamespace = (await sha256(data.HookNamespace)).toUpperCase();
 | 
			
		||||
  const hookOnValues: (keyof TTS)[] = data.Invoke.map((tt) => tt.value);
 | 
			
		||||
  const { HookParameters } = data;
 | 
			
		||||
  const filteredHookParameters = HookParameters.filter(
 | 
			
		||||
    (hp) =>
 | 
			
		||||
      hp.HookParameter.HookParameterName && hp.HookParameter.HookParameterValue
 | 
			
		||||
  )?.map((aa) => ({
 | 
			
		||||
    HookParameter: {
 | 
			
		||||
      HookParameterName: toHex(aa.HookParameter.HookParameterName || ""),
 | 
			
		||||
      HookParameterValue: aa.HookParameter.HookParameterValue || "",
 | 
			
		||||
    },
 | 
			
		||||
  }));
 | 
			
		||||
  // const filteredHookGrants = HookGrants.filter(hg => hg.HookGrant.Authorize || hg.HookGrant.HookHash).map(hg => {
 | 
			
		||||
  //   return {
 | 
			
		||||
  //     HookGrant: {
 | 
			
		||||
  //       ...(hg.HookGrant.Authorize && { Authorize: hg.HookGrant.Authorize }),
 | 
			
		||||
  //       // HookHash: hg.HookGrant.HookHash || undefined
 | 
			
		||||
  //       ...(hg.HookGrant.HookHash && { HookHash: hg.HookGrant.HookHash })
 | 
			
		||||
  //     }
 | 
			
		||||
  //   }
 | 
			
		||||
  // });
 | 
			
		||||
  if (typeof window !== "undefined") {
 | 
			
		||||
    const tx = {
 | 
			
		||||
      Account: account.address,
 | 
			
		||||
      TransactionType: "SetHook",
 | 
			
		||||
      Sequence: account.sequence,
 | 
			
		||||
      Fee: data.Fee,
 | 
			
		||||
      Hooks: [
 | 
			
		||||
        {
 | 
			
		||||
          Hook: {
 | 
			
		||||
            CreateCode: arrayBufferToHex(
 | 
			
		||||
              activeFile?.compiledContent
 | 
			
		||||
            ).toUpperCase(),
 | 
			
		||||
            HookOn: calculateHookOn(hookOnValues),
 | 
			
		||||
            HookNamespace,
 | 
			
		||||
            HookApiVersion: 0,
 | 
			
		||||
            Flags: 1,
 | 
			
		||||
            // ...(filteredHookGrants.length > 0 && { HookGrants: filteredHookGrants }),
 | 
			
		||||
            ...(filteredHookParameters.length > 0 && {
 | 
			
		||||
              HookParameters: filteredHookParameters,
 | 
			
		||||
            }),
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    };
 | 
			
		||||
    return tx;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/* deployHook function turns the wasm binary into
 | 
			
		||||
 * hex string, signs the transaction and deploys it to
 | 
			
		||||
 * Hooks testnet.
 | 
			
		||||
 */
 | 
			
		||||
export const deployHook = async (
 | 
			
		||||
  account: IAccount & { name?: string },
 | 
			
		||||
  data: SetHookData
 | 
			
		||||
) => {
 | 
			
		||||
  if (typeof window !== "undefined") {
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
    if (!state.client) {
 | 
			
		||||
      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 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}{" "}
 | 
			
		||||
              Transaction hash:{" "}
 | 
			
		||||
              <Link
 | 
			
		||||
                as="a"
 | 
			
		||||
                href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${submitRes.tx_json?.hash}`}
 | 
			
		||||
                target="_blank"
 | 
			
		||||
                rel="noopener noreferrer"
 | 
			
		||||
              >
 | 
			
		||||
                {submitRes.tx_json?.hash}
 | 
			
		||||
              </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 occurred while deploying",
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    if (currentAccount) {
 | 
			
		||||
      currentAccount.isLoading = false;
 | 
			
		||||
    }
 | 
			
		||||
    return submitRes;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const deleteHook = async (account: IAccount & { name?: string }) => {
 | 
			
		||||
  if (!state.client) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  const currentAccount = state.accounts.find(
 | 
			
		||||
    (acc) => acc.address === account.address
 | 
			
		||||
  );
 | 
			
		||||
  if (currentAccount?.isLoading || !currentAccount?.hooks.length) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (typeof window !== "undefined") {
 | 
			
		||||
    const tx = {
 | 
			
		||||
      Account: account.address,
 | 
			
		||||
      TransactionType: "SetHook",
 | 
			
		||||
      Sequence: account.sequence,
 | 
			
		||||
      Fee: "100000",
 | 
			
		||||
      Hooks: [
 | 
			
		||||
        {
 | 
			
		||||
          Hook: {
 | 
			
		||||
            CreateCode: "",
 | 
			
		||||
            Flags: 1,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const keypair = derive.familySeed(account.secret);
 | 
			
		||||
    try {
 | 
			
		||||
      // Update tx Fee value with network estimation
 | 
			
		||||
      const res = await estimateFee(tx, account);
 | 
			
		||||
      tx["Fee"] = res?.base_fee ? res?.base_fee : "1000";
 | 
			
		||||
    } 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 occurred while deleting hook", { id: toastId });
 | 
			
		||||
      state.deployLogs.push({
 | 
			
		||||
        type: "error",
 | 
			
		||||
        message: "Error occurred while deleting hook",
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    if (currentAccount) {
 | 
			
		||||
      currentAccount.isLoading = false;
 | 
			
		||||
    }
 | 
			
		||||
    return submitRes;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
@@ -8,11 +8,12 @@ export const downloadAsZip = async () => {
 | 
			
		||||
        state.zipLoading = true
 | 
			
		||||
        // TODO do something about file/gist loading state
 | 
			
		||||
        const files = state.files.map(({ name, content }) => ({ name, content }));
 | 
			
		||||
        const zipped = await createZip(files);
 | 
			
		||||
        const wasmFiles = state.files.filter(i => i.compiledContent).map(({ name, compiledContent }) => ({ name: `${name}.wasm`, content: compiledContent }));
 | 
			
		||||
        const zipped = await createZip([...files, ...wasmFiles]);
 | 
			
		||||
        const zipFileName = guessZipFileName(files);
 | 
			
		||||
        zipped.saveFile(zipFileName);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        toast.error('Error occured while creating zip file, try again later')
 | 
			
		||||
        toast.error('Error occurred while creating zip file, try again later')
 | 
			
		||||
    } finally {
 | 
			
		||||
        state.zipLoading = false
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,6 @@ import Router from "next/router";
 | 
			
		||||
import state from '../index';
 | 
			
		||||
import { templateFileIds } from '../constants';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const octokit = new Octokit();
 | 
			
		||||
 | 
			
		||||
/* Fetches Gist files from Githug Gists based on
 | 
			
		||||
@@ -19,24 +18,71 @@ export const fetchFiles = (gistId: string) => {
 | 
			
		||||
 | 
			
		||||
    octokit
 | 
			
		||||
      .request("GET /gists/{gist_id}", { gist_id: gistId })
 | 
			
		||||
      .then(res => {
 | 
			
		||||
        if (!Object.values(templateFileIds).includes(gistId)) {
 | 
			
		||||
      .then(async res => {
 | 
			
		||||
        if (!Object.values(templateFileIds).map(v => v.id).includes(gistId)) {
 | 
			
		||||
          return res
 | 
			
		||||
        }
 | 
			
		||||
        // in case of templates, fetch header file(s) and append to res
 | 
			
		||||
        return octokit.request("GET /gists/{gist_id}", { gist_id: templateFileIds.headers }).then(({ data: { files: headerFiles } }) => {
 | 
			
		||||
          const files = { ...res.data.files, ...headerFiles }
 | 
			
		||||
          res.data.files = files
 | 
			
		||||
          return res
 | 
			
		||||
        })
 | 
			
		||||
        try {
 | 
			
		||||
          const resHeader = await fetch(`${process.env.NEXT_PUBLIC_COMPILE_API_BASE_URL}/api/header-files`);
 | 
			
		||||
          if (resHeader.ok) {
 | 
			
		||||
            const resHeaderJson = await resHeader.json()
 | 
			
		||||
            const headerFiles: Record<string, { filename: string; content: string; language: string }> = {};
 | 
			
		||||
            Object.entries(resHeaderJson).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;
 | 
			
		||||
          }
 | 
			
		||||
        } 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 || "noname.c",
 | 
			
		||||
            name: res.data.files?.[filename]?.filename || "untitled.c",
 | 
			
		||||
            language: res.data.files?.[filename]?.language?.toLowerCase() || "",
 | 
			
		||||
            content: res.data.files?.[filename]?.content || "",
 | 
			
		||||
          }));
 | 
			
		||||
          // Sort files so that the source files are first
 | 
			
		||||
          // In case of other files leave the order as it its
 | 
			
		||||
          files.sort((a, b) => {
 | 
			
		||||
            const aBasename = a.name.split('.')?.[0];
 | 
			
		||||
            const aCext = a.name?.toLowerCase().endsWith('.c');
 | 
			
		||||
            const bBasename = b.name.split('.')?.[0];
 | 
			
		||||
            const bCext = b.name?.toLowerCase().endsWith('.c');
 | 
			
		||||
            // If a has c extension and b doesn't move a up
 | 
			
		||||
            if (aCext && !bCext) {
 | 
			
		||||
              return -1;
 | 
			
		||||
            }
 | 
			
		||||
            if (!aCext && bCext) {
 | 
			
		||||
              return 1
 | 
			
		||||
            }
 | 
			
		||||
            // Otherwise fallback to default sorting based on basename
 | 
			
		||||
            if (aBasename > bBasename) {
 | 
			
		||||
              return 1;
 | 
			
		||||
            }
 | 
			
		||||
            if (bBasename > aBasename) {
 | 
			
		||||
              return -1;
 | 
			
		||||
            }
 | 
			
		||||
            return 0;
 | 
			
		||||
          })
 | 
			
		||||
          state.loading = false;
 | 
			
		||||
          if (files.length > 0) {
 | 
			
		||||
            state.logs.push({
 | 
			
		||||
@@ -68,4 +114,4 @@ export const fetchFiles = (gistId: string) => {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  state.loading = false;
 | 
			
		||||
};
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,29 +1,40 @@
 | 
			
		||||
import toast from "react-hot-toast";
 | 
			
		||||
import { derive } from "xrpl-accountlib";
 | 
			
		||||
import { derive, XRPL_Account } from "xrpl-accountlib";
 | 
			
		||||
 | 
			
		||||
import state from '../index';
 | 
			
		||||
import { names } from './addFaucetAccount';
 | 
			
		||||
 | 
			
		||||
// Adds test account to global state with secret key
 | 
			
		||||
export const importAccount = (secret: string) => {
 | 
			
		||||
export const importAccount = (secret: string, name?: string) => {
 | 
			
		||||
  if (!secret) {
 | 
			
		||||
    return toast.error("You need to add secret!");
 | 
			
		||||
  }
 | 
			
		||||
  if (state.accounts.find((acc) => acc.secret === secret)) {
 | 
			
		||||
    return toast.error("Account already added!");
 | 
			
		||||
  }
 | 
			
		||||
  const account = derive.familySeed(secret);
 | 
			
		||||
  if (!account.secret.familySeed) {
 | 
			
		||||
  let account: XRPL_Account | null = null;
 | 
			
		||||
  try {
 | 
			
		||||
    account = derive.familySeed(secret);
 | 
			
		||||
  } catch (err: any) {
 | 
			
		||||
    if (err?.message) {
 | 
			
		||||
      toast.error(err.message)
 | 
			
		||||
    } else {
 | 
			
		||||
      toast.error('Error occurred while importing account')
 | 
			
		||||
    }
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!account || !account.secret.familySeed) {
 | 
			
		||||
    return toast.error(`Couldn't create account!`);
 | 
			
		||||
  }
 | 
			
		||||
  state.accounts.push({
 | 
			
		||||
    name: names[state.accounts.length],
 | 
			
		||||
    name: name || names[state.accounts.length],
 | 
			
		||||
    address: account.address || "",
 | 
			
		||||
    secret: account.secret.familySeed || "",
 | 
			
		||||
    xrp: "0",
 | 
			
		||||
    sequence: 1,
 | 
			
		||||
    hooks: [],
 | 
			
		||||
    isLoading: false,
 | 
			
		||||
    version: '2'
 | 
			
		||||
  });
 | 
			
		||||
  return toast.success("Account imported successfully!");
 | 
			
		||||
};
 | 
			
		||||
@@ -2,16 +2,27 @@ import toast from "react-hot-toast";
 | 
			
		||||
import state from '../index';
 | 
			
		||||
 | 
			
		||||
// 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 sought = '/' + state.files[state.active].name;
 | 
			
		||||
  const currentModel = editorModels?.find((editorModel) => {
 | 
			
		||||
    return editorModel.uri.path.endsWith(sought);
 | 
			
		||||
  });
 | 
			
		||||
  const file = state.files[activeId || state.active]
 | 
			
		||||
  if (state.files.length > 0) {
 | 
			
		||||
    state.files[state.active].content = currentModel?.getValue() || "";
 | 
			
		||||
    file.content = currentModel?.getValue() || "";
 | 
			
		||||
  }
 | 
			
		||||
  if (showToast) {
 | 
			
		||||
    toast.success("Saved successfully", { position: "bottom-center" });
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const saveAllFiles = () => {
 | 
			
		||||
  const editorModels = state.editorCtx?.getModels();
 | 
			
		||||
  state.files.forEach(file => {
 | 
			
		||||
    const currentModel = editorModels?.find(model => model.uri.path.endsWith('/' + file.name))
 | 
			
		||||
    if (currentModel) {
 | 
			
		||||
      file.content = currentModel?.getValue() || '';
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -20,8 +20,8 @@ export const sendTransaction = async (account: IAccount, txOptions: TransactionO
 | 
			
		||||
    const { Fee = "1000", ...opts } = txOptions
 | 
			
		||||
    const tx: TransactionOptions = {
 | 
			
		||||
        Account: account.address,
 | 
			
		||||
        Sequence: account.sequence, // TODO auto-fillable
 | 
			
		||||
        Fee,  // TODO auto-fillable
 | 
			
		||||
        Sequence: account.sequence,
 | 
			
		||||
        Fee,  // TODO auto-fillable default
 | 
			
		||||
        ...opts
 | 
			
		||||
    };
 | 
			
		||||
    const { logPrefix = '' } = options || {}
 | 
			
		||||
@@ -43,6 +43,10 @@ export const sendTransaction = async (account: IAccount, txOptions: TransactionO
 | 
			
		||||
                message: `${logPrefix}[${response.error || response.engine_result}] ${response.error_exception || response.engine_result_message}`,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        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({
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								state/actions/showAlert.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								state/actions/showAlert.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
import { ref } from 'valtio';
 | 
			
		||||
import { AlertState, alertState } from "../../components/AlertDialog";
 | 
			
		||||
 | 
			
		||||
export const showAlert = (title: string, opts: Omit<Partial<AlertState>, 'title' | 'isOpen'> = {}) => {
 | 
			
		||||
    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 = {
 | 
			
		||||
        isOpen: true,
 | 
			
		||||
        title,
 | 
			
		||||
        body,
 | 
			
		||||
        confirmPrefix,
 | 
			
		||||
        cancelText: undefined,
 | 
			
		||||
        confirmText: undefined,
 | 
			
		||||
        onCancel: undefined,
 | 
			
		||||
        onConfirm: undefined,
 | 
			
		||||
        ...rest,
 | 
			
		||||
    }
 | 
			
		||||
    Object.entries(nwState).forEach(([key, value]) => {
 | 
			
		||||
        (alertState as any)[key] = value
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
@@ -4,6 +4,7 @@ import { Octokit } from "@octokit/core";
 | 
			
		||||
import Router from "next/router";
 | 
			
		||||
 | 
			
		||||
import state from '../index';
 | 
			
		||||
import { saveAllFiles } from "./saveFile";
 | 
			
		||||
 | 
			
		||||
const octokit = new Octokit();
 | 
			
		||||
 | 
			
		||||
@@ -12,6 +13,7 @@ export const syncToGist = async (
 | 
			
		||||
  session?: Session | null,
 | 
			
		||||
  createNewGist?: boolean
 | 
			
		||||
) => {
 | 
			
		||||
  saveAllFiles();
 | 
			
		||||
  let files: Record<string, { filename: string; content: string }> = {};
 | 
			
		||||
  state.gistLoading = true;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,41 @@
 | 
			
		||||
import Carbon from "../../components/icons/Carbon";
 | 
			
		||||
import Firewall from "../../components/icons/Firewall";
 | 
			
		||||
import Notary from "../../components/icons/Notary";
 | 
			
		||||
import Peggy from "../../components/icons/Peggy";
 | 
			
		||||
import Starter from "../../components/icons/Starter";
 | 
			
		||||
 | 
			
		||||
export const templateFileIds = {
 | 
			
		||||
    'starter': '1d14e51e2e02dc0a508cb0733767a914', // TODO currently same as accept
 | 
			
		||||
    'firewall': 'bcd6d0c0fcbe52545ddb802481ff9d26',
 | 
			
		||||
    'notary': 'a789c75f591eeab7932fd702ed8cf9ea',
 | 
			
		||||
    'carbon': '43925143fa19735d8c6505c34d3a6a47',
 | 
			
		||||
    'peggy': 'ceaf352e2a65741341033ab7ef05c448',
 | 
			
		||||
    'headers': '9b448e8a55fab11ef5d1274cb59f9cf3'
 | 
			
		||||
    'starter': {
 | 
			
		||||
        id: '9106f1fe60482d90475bfe8f1315affe',
 | 
			
		||||
        name: 'Starter',
 | 
			
		||||
        description: 'Just a basic starter with essential imports, just accepts any transaction coming through',
 | 
			
		||||
        icon: Starter
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    'firewall': {
 | 
			
		||||
        id: '1cc30f39c8a0b9c55b88c312669ca45e',  // Forked
 | 
			
		||||
        name: 'Firewall',
 | 
			
		||||
        description: 'This Hook essentially checks a blacklist of accounts',
 | 
			
		||||
        icon: Firewall
 | 
			
		||||
    },
 | 
			
		||||
    'notary': {
 | 
			
		||||
        id: '87b6f5a8c2f5038fb0f20b8b510efa10', // Forked
 | 
			
		||||
        name: 'Notary',
 | 
			
		||||
        description: 'Collecting signatures for multi-sign transactions',
 | 
			
		||||
        icon: Notary
 | 
			
		||||
    },
 | 
			
		||||
    'carbon': {
 | 
			
		||||
        id: '5941c19dce3e147948f564e224553c02',
 | 
			
		||||
        name: 'Carbon',
 | 
			
		||||
        description: 'Send a percentage of sum to an address',
 | 
			
		||||
        icon: Carbon
 | 
			
		||||
    },
 | 
			
		||||
    'peggy': {
 | 
			
		||||
        id: '049784a83fa068faf7912f663f7b6471', // Forked
 | 
			
		||||
        name: 'Peggy',
 | 
			
		||||
        description: 'An oracle based stable coin hook',
 | 
			
		||||
        icon: Peggy
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const apiHeaderFiles = ['hookapi.h', 'sfcodes.h', 'hookmacro.h']
 | 
			
		||||
export const apiHeaderFiles = ['hookapi.h', 'sfcodes.h', 'macro.h', 'extern.h', 'error.h'];
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,23 @@
 | 
			
		||||
import type monaco from "monaco-editor";
 | 
			
		||||
import { proxy, ref, subscribe } from "valtio";
 | 
			
		||||
import { devtools } from 'valtio/utils';
 | 
			
		||||
import { devtools, subscribeKey } from 'valtio/utils';
 | 
			
		||||
import { XrplClient } from "xrpl-client";
 | 
			
		||||
import { SplitSize } from "./actions/persistSplits";
 | 
			
		||||
 | 
			
		||||
declare module "valtio" {
 | 
			
		||||
  function useSnapshot<T extends object>(p: T): T;
 | 
			
		||||
  function snapshot<T extends object>(p: T): T;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IFile {
 | 
			
		||||
  name: string;
 | 
			
		||||
  language: string;
 | 
			
		||||
  content: string;
 | 
			
		||||
  compiledValueSnapshot?: string
 | 
			
		||||
  compiledContent?: ArrayBuffer | null;
 | 
			
		||||
  compiledWatContent?: string | null;
 | 
			
		||||
  lastCompiled?: Date
 | 
			
		||||
  containsErrors?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface FaucetAccountRes {
 | 
			
		||||
@@ -29,18 +36,26 @@ export interface IAccount {
 | 
			
		||||
  sequence: number;
 | 
			
		||||
  hooks: string[];
 | 
			
		||||
  isLoading: boolean;
 | 
			
		||||
  version?: string;
 | 
			
		||||
  error?: {
 | 
			
		||||
    message: string;
 | 
			
		||||
    code: string;
 | 
			
		||||
  } | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ILog {
 | 
			
		||||
  type: "error" | "warning" | "log" | "success";
 | 
			
		||||
  message: string;
 | 
			
		||||
  message: string | JSX.Element;
 | 
			
		||||
  key?: string;
 | 
			
		||||
  jsonData?: any,
 | 
			
		||||
  timestamp?: string;
 | 
			
		||||
  timestring?: string;
 | 
			
		||||
  link?: string;
 | 
			
		||||
  linkText?: string;
 | 
			
		||||
  defaultCollapsed?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type DeployValue = Record<IFile['name'], any>;
 | 
			
		||||
 | 
			
		||||
export interface IState {
 | 
			
		||||
  files: IFile[];
 | 
			
		||||
  gistId?: string | null;
 | 
			
		||||
@@ -55,6 +70,7 @@ export interface IState {
 | 
			
		||||
  logs: ILog[];
 | 
			
		||||
  deployLogs: ILog[];
 | 
			
		||||
  transactionLogs: ILog[];
 | 
			
		||||
  scriptLogs: ILog[];
 | 
			
		||||
  editorCtx?: typeof monaco.editor;
 | 
			
		||||
  editorSettings: {
 | 
			
		||||
    tabSize: number;
 | 
			
		||||
@@ -65,7 +81,13 @@ export interface IState {
 | 
			
		||||
  client: XrplClient | null;
 | 
			
		||||
  clientStatus: "offline" | "online";
 | 
			
		||||
  mainModalOpen: boolean;
 | 
			
		||||
  mainModalShowed: boolean;
 | 
			
		||||
  accounts: IAccount[];
 | 
			
		||||
  compileOptions: {
 | 
			
		||||
    optimizationLevel: '-O0' | '-O1' | '-O2' | '-O3' | '-O4' | '-Os';
 | 
			
		||||
    strip: boolean
 | 
			
		||||
  },
 | 
			
		||||
  deployValues: DeployValue
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// let localStorageState: null | string = null;
 | 
			
		||||
@@ -80,6 +102,7 @@ let initialState: IState = {
 | 
			
		||||
  logs: [],
 | 
			
		||||
  deployLogs: [],
 | 
			
		||||
  transactionLogs: [],
 | 
			
		||||
  scriptLogs: [],
 | 
			
		||||
  editorCtx: undefined,
 | 
			
		||||
  gistId: undefined,
 | 
			
		||||
  gistOwner: undefined,
 | 
			
		||||
@@ -93,7 +116,13 @@ let initialState: IState = {
 | 
			
		||||
  client: null,
 | 
			
		||||
  clientStatus: "offline" as "offline",
 | 
			
		||||
  mainModalOpen: false,
 | 
			
		||||
  mainModalShowed: false,
 | 
			
		||||
  accounts: [],
 | 
			
		||||
  compileOptions: {
 | 
			
		||||
    optimizationLevel: '-O2',
 | 
			
		||||
    strip: true
 | 
			
		||||
  },
 | 
			
		||||
  deployValues: {}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
let localStorageAccounts: string | null = null;
 | 
			
		||||
@@ -112,6 +141,8 @@ if (typeof window !== "undefined") {
 | 
			
		||||
  if (localStorageAccounts) {
 | 
			
		||||
    initialAccounts = JSON.parse(localStorageAccounts);
 | 
			
		||||
  }
 | 
			
		||||
  // filter out old accounts (they do not have version property at all)
 | 
			
		||||
  // initialAccounts = initialAccounts.filter(acc => acc.version === '2');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Initialize state
 | 
			
		||||
@@ -120,9 +151,8 @@ const state = proxy<IState>({
 | 
			
		||||
  accounts: initialAccounts.length > 0 ? initialAccounts : [],
 | 
			
		||||
  logs: [],
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Initialize socket connection
 | 
			
		||||
const client = new XrplClient("wss://hooks-testnet.xrpl-labs.com");
 | 
			
		||||
const client = new XrplClient(`wss://${process.env.NEXT_PUBLIC_TESTNET_URL}`);
 | 
			
		||||
 | 
			
		||||
client.on("online", () => {
 | 
			
		||||
  state.client = ref(client);
 | 
			
		||||
@@ -138,15 +168,24 @@ if (process.env.NODE_ENV !== "production") {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (typeof window !== "undefined") {
 | 
			
		||||
  subscribe(state, () => {
 | 
			
		||||
    const { accounts, active } = state;
 | 
			
		||||
  subscribe(state.accounts, () => {
 | 
			
		||||
    const { accounts } = state;
 | 
			
		||||
    const accountsNoLoading = accounts.map(acc => ({ ...acc, isLoading: false }))
 | 
			
		||||
    localStorage.setItem("hooksIdeAccounts", JSON.stringify(accountsNoLoading));
 | 
			
		||||
    if (!state.files[active]?.compiledWatContent) {
 | 
			
		||||
      state.activeWat = 0;
 | 
			
		||||
    } else {
 | 
			
		||||
      state.activeWat = active;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const updateActiveWat = () => {
 | 
			
		||||
    const filename = state.files[state.active]?.name
 | 
			
		||||
 | 
			
		||||
    const compiledFiles = state.files.filter(
 | 
			
		||||
      file => file.compiledContent)
 | 
			
		||||
    const idx = compiledFiles.findIndex(file => file.name === filename)
 | 
			
		||||
 | 
			
		||||
    if (idx !== -1) state.activeWat = idx
 | 
			
		||||
  }
 | 
			
		||||
  subscribeKey(state, 'active', updateActiveWat)
 | 
			
		||||
  subscribeKey(state, 'files', updateActiveWat)
 | 
			
		||||
}
 | 
			
		||||
export default state
 | 
			
		||||
 | 
			
		||||
export * from './transactions'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										260
									
								
								state/transactions.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										260
									
								
								state/transactions.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,260 @@
 | 
			
		||||
import { proxy } from 'valtio';
 | 
			
		||||
import { deepEqual } from '../utils/object';
 | 
			
		||||
import transactionsData from "../content/transactions.json";
 | 
			
		||||
import state from '.';
 | 
			
		||||
import { showAlert } from "../state/actions/showAlert";
 | 
			
		||||
import { parseJSON } from '../utils/json';
 | 
			
		||||
 | 
			
		||||
export type SelectOption = {
 | 
			
		||||
    value: string;
 | 
			
		||||
    label: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface TransactionState {
 | 
			
		||||
    selectedTransaction: SelectOption | null;
 | 
			
		||||
    selectedAccount: SelectOption | null;
 | 
			
		||||
    selectedDestAccount: SelectOption | null;
 | 
			
		||||
    txIsLoading: boolean;
 | 
			
		||||
    txIsDisabled: boolean;
 | 
			
		||||
    txFields: TxFields;
 | 
			
		||||
    viewType: 'json' | 'ui',
 | 
			
		||||
    editorValue?: string,
 | 
			
		||||
    estimatedFee?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type TxFields = Omit<
 | 
			
		||||
    Partial<typeof transactionsData[0]>,
 | 
			
		||||
    "Account" | "Sequence" | "TransactionType"
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
export const defaultTransaction: TransactionState = {
 | 
			
		||||
    selectedTransaction: null,
 | 
			
		||||
    selectedAccount: null,
 | 
			
		||||
    selectedDestAccount: null,
 | 
			
		||||
    txIsLoading: false,
 | 
			
		||||
    txIsDisabled: false,
 | 
			
		||||
    txFields: {},
 | 
			
		||||
    viewType: 'ui'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const transactionsState = proxy({
 | 
			
		||||
    transactions: [
 | 
			
		||||
        {
 | 
			
		||||
            header: "test1.json",
 | 
			
		||||
            state: { ...defaultTransaction },
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    activeHeader: "test1.json"
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const renameTxState = (oldName: string, nwName: string) => {
 | 
			
		||||
    const tx = transactionsState.transactions.find(tx => tx.header === oldName);
 | 
			
		||||
 | 
			
		||||
    if (!tx) throw Error(`No transaction state exists with given header name ${oldName}`);
 | 
			
		||||
 | 
			
		||||
    tx.header = nwName
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Simple transaction state changer
 | 
			
		||||
 * @param header Unique key and tab name for the transaction tab
 | 
			
		||||
 * @param partialTx partial transaction state, `undefined` deletes the transaction
 | 
			
		||||
 * 
 | 
			
		||||
 */
 | 
			
		||||
export const modifyTxState = (
 | 
			
		||||
    header: string,
 | 
			
		||||
    partialTx?: Partial<TransactionState>,
 | 
			
		||||
    opts: { replaceState?: boolean } = {}
 | 
			
		||||
) => {
 | 
			
		||||
    const tx = transactionsState.transactions.find(tx => tx.header === header);
 | 
			
		||||
 | 
			
		||||
    if (partialTx === undefined) {
 | 
			
		||||
        transactionsState.transactions = transactionsState.transactions.filter(
 | 
			
		||||
            tx => tx.header !== header
 | 
			
		||||
        );
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!tx) {
 | 
			
		||||
        const state = {
 | 
			
		||||
            ...defaultTransaction,
 | 
			
		||||
            ...partialTx,
 | 
			
		||||
        }
 | 
			
		||||
        transactionsState.transactions.push({
 | 
			
		||||
            header,
 | 
			
		||||
            state,
 | 
			
		||||
        });
 | 
			
		||||
        return state;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (opts.replaceState) {
 | 
			
		||||
        const repTx: TransactionState = {
 | 
			
		||||
            ...defaultTransaction,
 | 
			
		||||
            ...partialTx,
 | 
			
		||||
        }
 | 
			
		||||
        tx.state = repTx
 | 
			
		||||
        return repTx
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Object.keys(partialTx).forEach(k => {
 | 
			
		||||
        // Typescript mess here, but is definitely safe!
 | 
			
		||||
        const s = tx.state as any;
 | 
			
		||||
        const p = partialTx as any; // ? Make copy
 | 
			
		||||
        if (!deepEqual(s[k], p[k])) s[k] = p[k];
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return tx.state
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// state to tx options
 | 
			
		||||
export const prepareTransaction = (data: any) => {
 | 
			
		||||
    let options = { ...data };
 | 
			
		||||
 | 
			
		||||
    (Object.keys(options)).forEach(field => {
 | 
			
		||||
        let _value = options[field];
 | 
			
		||||
        // convert xrp
 | 
			
		||||
        if (_value && typeof _value === "object" && _value.$type === "xrp") {
 | 
			
		||||
            if (+_value.$value) {
 | 
			
		||||
                options[field] = (+_value.$value * 1000000 + "") as any;
 | 
			
		||||
            } else {
 | 
			
		||||
                options[field] = undefined; // 👇 💀
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        // handle type: `json`
 | 
			
		||||
        if (_value && typeof _value === "object" && _value.$type === "json") {
 | 
			
		||||
            if (typeof _value.$value === "object") {
 | 
			
		||||
                options[field] = _value.$value;
 | 
			
		||||
            } else {
 | 
			
		||||
                try {
 | 
			
		||||
                    options[field] = JSON.parse(_value.$value);
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    const message = `Input error for json field '${field}': ${error instanceof Error ? error.message : ""
 | 
			
		||||
                        }`;
 | 
			
		||||
                    console.error(message)
 | 
			
		||||
                    options[field] = _value.$value
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // delete unnecessary fields
 | 
			
		||||
        if (!options[field]) {
 | 
			
		||||
            delete options[field];
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return options
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// editor value to state
 | 
			
		||||
export const prepareState = (value: string, transactionType?: string) => {
 | 
			
		||||
    const options = parseJSON(value);
 | 
			
		||||
    if (!options) {
 | 
			
		||||
        showAlert("Error!", {
 | 
			
		||||
            body: "Cannot save editor with malformed transaction."
 | 
			
		||||
        })
 | 
			
		||||
        return
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const { Account, TransactionType, Destination, ...rest } = options;
 | 
			
		||||
    let tx: Partial<TransactionState> = {};
 | 
			
		||||
    const schema = getTxFields(transactionType)
 | 
			
		||||
 | 
			
		||||
    if (Account) {
 | 
			
		||||
        const acc = state.accounts.find(acc => acc.address === Account);
 | 
			
		||||
        if (acc) {
 | 
			
		||||
            tx.selectedAccount = {
 | 
			
		||||
                label: acc.name,
 | 
			
		||||
                value: acc.address,
 | 
			
		||||
            };
 | 
			
		||||
        } else {
 | 
			
		||||
            tx.selectedAccount = {
 | 
			
		||||
                label: Account,
 | 
			
		||||
                value: Account,
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        tx.selectedAccount = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (TransactionType) {
 | 
			
		||||
        tx.selectedTransaction = {
 | 
			
		||||
            label: TransactionType,
 | 
			
		||||
            value: TransactionType,
 | 
			
		||||
        };
 | 
			
		||||
    } else {
 | 
			
		||||
        tx.selectedTransaction = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (schema.Destination !== undefined) {
 | 
			
		||||
        const dest = state.accounts.find(acc => acc.address === Destination);
 | 
			
		||||
        if (dest) {
 | 
			
		||||
            tx.selectedDestAccount = {
 | 
			
		||||
                label: dest.name,
 | 
			
		||||
                value: dest.address,
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        else if (Destination) {
 | 
			
		||||
            tx.selectedDestAccount = {
 | 
			
		||||
                label: Destination,
 | 
			
		||||
                value: Destination,
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            tx.selectedDestAccount = null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    else if (Destination) {
 | 
			
		||||
        rest.Destination = Destination
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Object.keys(rest).forEach(field => {
 | 
			
		||||
        const value = rest[field];
 | 
			
		||||
        const schemaVal = schema[field as keyof TxFields]
 | 
			
		||||
        const isXrp = typeof value !== 'object' && schemaVal && typeof schemaVal === 'object' && schemaVal.$type === 'xrp'
 | 
			
		||||
        if (isXrp) {
 | 
			
		||||
            rest[field] = {
 | 
			
		||||
                $type: "xrp",
 | 
			
		||||
                $value: +value / 1000000, // ! maybe use bigint?
 | 
			
		||||
            };
 | 
			
		||||
        } else if (typeof value === "object") {
 | 
			
		||||
            rest[field] = {
 | 
			
		||||
                $type: "json",
 | 
			
		||||
                $value: value,
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    tx.txFields = rest;
 | 
			
		||||
 | 
			
		||||
    return tx
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getTxFields = (tt?: string) => {
 | 
			
		||||
    const txFields: TxFields | undefined = transactionsData.find(
 | 
			
		||||
        tx => tx.TransactionType === tt
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (!txFields) return {}
 | 
			
		||||
 | 
			
		||||
    let _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
 | 
			
		||||
            ),
 | 
			
		||||
            {}
 | 
			
		||||
        );
 | 
			
		||||
    return _txFields
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { transactionsData }
 | 
			
		||||
 | 
			
		||||
export const transactionsOptions = transactionsData.map(tx => ({
 | 
			
		||||
    value: tx.TransactionType,
 | 
			
		||||
    label: tx.TransactionType,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export const defaultTransactionType = transactionsOptions.find(tt => tt.value === 'Payment')
 | 
			
		||||
@@ -9,16 +9,20 @@ import {
 | 
			
		||||
  grass,
 | 
			
		||||
  slate,
 | 
			
		||||
  mauve,
 | 
			
		||||
  mauveA,
 | 
			
		||||
  amber,
 | 
			
		||||
  purple,
 | 
			
		||||
  green,
 | 
			
		||||
  grayDark,
 | 
			
		||||
  blueDark,
 | 
			
		||||
  crimsonDark,
 | 
			
		||||
  grassDark,
 | 
			
		||||
  slateDark,
 | 
			
		||||
  mauveDark,
 | 
			
		||||
  mauveDarkA,
 | 
			
		||||
  amberDark,
 | 
			
		||||
  purpleDark,
 | 
			
		||||
  greenDark,
 | 
			
		||||
  red,
 | 
			
		||||
  redDark,
 | 
			
		||||
} from "@radix-ui/colors";
 | 
			
		||||
@@ -41,12 +45,15 @@ export const {
 | 
			
		||||
      ...grass,
 | 
			
		||||
      ...slate,
 | 
			
		||||
      ...mauve,
 | 
			
		||||
      ...mauveA,
 | 
			
		||||
      ...amber,
 | 
			
		||||
      ...purple,
 | 
			
		||||
      ...green,
 | 
			
		||||
      ...red,
 | 
			
		||||
      accent: "#9D2DFF",
 | 
			
		||||
      background: "$gray1",
 | 
			
		||||
      backgroundAlt: "$gray4",
 | 
			
		||||
      backgroundOverlay: "$mauve2",
 | 
			
		||||
      text: "$gray12",
 | 
			
		||||
      textMuted: "$gray10",
 | 
			
		||||
      primary: "$plum",
 | 
			
		||||
@@ -353,10 +360,13 @@ export const darkTheme = createTheme("dark", {
 | 
			
		||||
    ...grassDark,
 | 
			
		||||
    ...slateDark,
 | 
			
		||||
    ...mauveDark,
 | 
			
		||||
    ...mauveDarkA,
 | 
			
		||||
    ...amberDark,
 | 
			
		||||
    ...purpleDark,
 | 
			
		||||
    ...greenDark,
 | 
			
		||||
    ...redDark,
 | 
			
		||||
    deep: "rgb(10, 10, 10)",
 | 
			
		||||
    backgroundOverlay: "$mauve5"
 | 
			
		||||
    // backgroundA: transparentize(0.1, grayDark.gray1),
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ body,
 | 
			
		||||
  min-height: 100vh;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  overflow-y: hidden;
 | 
			
		||||
  /* overflow-y: hidden; */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
* {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								styles/keyframes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								styles/keyframes.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
import { keyframes } from '../stitches.config';
 | 
			
		||||
 | 
			
		||||
export const slideUpAndFade = keyframes({
 | 
			
		||||
    "0%": { opacity: 0, transform: "translateY(2px)" },
 | 
			
		||||
    "100%": { opacity: 1, transform: "translateY(0)" },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const slideRightAndFade = keyframes({
 | 
			
		||||
    "0%": { opacity: 0, transform: "translateX(-2px)" },
 | 
			
		||||
    "100%": { opacity: 1, transform: "translateX(0)" },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const slideDownAndFade = keyframes({
 | 
			
		||||
    "0%": { opacity: 0, transform: "translateY(-2px)" },
 | 
			
		||||
    "100%": { opacity: 1, transform: "translateY(0)" },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const slideLeftAndFade = keyframes({
 | 
			
		||||
    "0%": { opacity: 0, transform: "translateX(2px)" },
 | 
			
		||||
    "100%": { opacity: 1, transform: "translateX(0)" },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										24
									
								
								utils/comment-parser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								utils/comment-parser.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
import { Spec, parse, Problem } from "comment-parser"
 | 
			
		||||
 | 
			
		||||
export const getTags = (source?: string): Spec[] => {
 | 
			
		||||
    if (!source) return []
 | 
			
		||||
    const blocks = parse(source)
 | 
			
		||||
    const tags = blocks.reduce(
 | 
			
		||||
        (acc, block) => acc.concat(block.tags),
 | 
			
		||||
        [] as Spec[]
 | 
			
		||||
    );
 | 
			
		||||
    return tags
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getErrors = (source?: string): Error | undefined => {
 | 
			
		||||
    if (!source) return undefined
 | 
			
		||||
    const blocks = parse(source)
 | 
			
		||||
    const probs = blocks.reduce(
 | 
			
		||||
        (acc, block) => acc.concat(block.problems),
 | 
			
		||||
        [] as Problem[]
 | 
			
		||||
    );
 | 
			
		||||
    if (!probs.length) return undefined
 | 
			
		||||
    const errors = probs.map(prob => `[${prob.code}] on line ${prob.line}: ${prob.message}`)
 | 
			
		||||
    const error = new Error(`The following error(s) occurred while parsing JSDOC: \n${errors.join('\n')}`)
 | 
			
		||||
    return error
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								utils/estimateFee.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								utils/estimateFee.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
import toast from 'react-hot-toast';
 | 
			
		||||
import { derive, sign } from "xrpl-accountlib"
 | 
			
		||||
import state, { IAccount } from "../state"
 | 
			
		||||
 | 
			
		||||
const estimateFee = async (tx: Record<string, unknown>, account: IAccount, opts: { silent?: boolean } = {}): Promise<null | { base_fee: string, median_fee: string; minimum_fee: string; open_ledger_fee: string; }> => {
 | 
			
		||||
  try {
 | 
			
		||||
    const copyTx = JSON.parse(JSON.stringify(tx))
 | 
			
		||||
    delete copyTx['SigningPubKey']
 | 
			
		||||
    if (!copyTx.Fee) {
 | 
			
		||||
      copyTx.Fee = '1000'
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    const keypair = derive.familySeed(account.secret)
 | 
			
		||||
    const { signedTransaction } = sign(copyTx, keypair);
 | 
			
		||||
 | 
			
		||||
    const res = await state.client?.send({ command: 'fee', tx_blob: signedTransaction })
 | 
			
		||||
    if (res && res.drops) {
 | 
			
		||||
      return res.drops;
 | 
			
		||||
    }
 | 
			
		||||
    return null
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    if (!opts.silent) {
 | 
			
		||||
      console.error(err)
 | 
			
		||||
      toast.error("Cannot estimate fee.") // ? Some better msg
 | 
			
		||||
    }
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default estimateFee
 | 
			
		||||
@@ -6,4 +6,10 @@ export const guessZipFileName = (files: File[]) => {
 | 
			
		||||
    let parts = (files.filter(f => f.name.endsWith('.c'))[0]?.name || 'hook').split('.')
 | 
			
		||||
    parts = parts.length > 1 ? parts.slice(0, -1) : parts
 | 
			
		||||
    return parts.join('')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const capitalize = (value?: string) => {
 | 
			
		||||
    if (!value) return '';
 | 
			
		||||
 | 
			
		||||
    return value[0].toLocaleUpperCase() + value.slice(1);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										46
									
								
								utils/hookOnCalculator.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								utils/hookOnCalculator.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
			
		||||
export const tts = {
 | 
			
		||||
  ttPAYMENT: 0,
 | 
			
		||||
  ttESCROW_CREATE: 1,
 | 
			
		||||
  ttESCROW_FINISH: 2,
 | 
			
		||||
  ttACCOUNT_SET: 3,
 | 
			
		||||
  ttESCROW_CANCEL: 4,
 | 
			
		||||
  ttREGULAR_KEY_SET: 5,
 | 
			
		||||
  ttOFFER_CREATE: 7,
 | 
			
		||||
  ttOFFER_CANCEL: 8,
 | 
			
		||||
  ttTICKET_CREATE: 10,
 | 
			
		||||
  ttSIGNER_LIST_SET: 12,
 | 
			
		||||
  ttPAYCHAN_CREATE: 13,
 | 
			
		||||
  ttPAYCHAN_FUND: 14,
 | 
			
		||||
  ttPAYCHAN_CLAIM: 15,
 | 
			
		||||
  ttCHECK_CREATE: 16,
 | 
			
		||||
  ttCHECK_CASH: 17,
 | 
			
		||||
  ttCHECK_CANCEL: 18,
 | 
			
		||||
  ttDEPOSIT_PREAUTH: 19,
 | 
			
		||||
  ttTRUST_SET: 20,
 | 
			
		||||
  ttACCOUNT_DELETE: 21,
 | 
			
		||||
  ttHOOK_SET: 22,
 | 
			
		||||
  ttNFTOKEN_MINT: 25,
 | 
			
		||||
  ttNFTOKEN_BURN: 26,
 | 
			
		||||
  ttNFTOKEN_CREATE_OFFER: 27,
 | 
			
		||||
  ttNFTOKEN_CANCEL_OFFER: 28,
 | 
			
		||||
  ttNFTOKEN_ACCEPT_OFFER: 29
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type TTS = typeof tts;
 | 
			
		||||
 | 
			
		||||
const calculateHookOn = (arr: (keyof TTS)[]) => {
 | 
			
		||||
  let start = '0x000000003e3ff5bf';
 | 
			
		||||
  arr.forEach(n => {
 | 
			
		||||
    let v = BigInt(start);
 | 
			
		||||
    v ^= (BigInt(1) << BigInt(tts[n as keyof TTS]));
 | 
			
		||||
    let s = v.toString(16);
 | 
			
		||||
    let l = s.length;
 | 
			
		||||
    if (l < 16)
 | 
			
		||||
      s = '0'.repeat(16 - l) + s;
 | 
			
		||||
    s = '0x' + s;
 | 
			
		||||
    start = s;
 | 
			
		||||
  })
 | 
			
		||||
  return start.substring(2);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default calculateHookOn
 | 
			
		||||
@@ -18,4 +18,14 @@ export const extractJSON = (str?: string) => {
 | 
			
		||||
        } while (firstClose > firstOpen);
 | 
			
		||||
        firstOpen = str.indexOf('{', firstOpen + 1);
 | 
			
		||||
    } while (firstOpen != -1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const parseJSON = (str?: string | null): any | undefined => {
 | 
			
		||||
    if (!str) return undefined
 | 
			
		||||
    try {
 | 
			
		||||
        const parsed = JSON.parse(str);
 | 
			
		||||
        return typeof parsed === "object" ? parsed : undefined;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        return undefined;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
import { MessageConnection } from "@codingame/monaco-jsonrpc";
 | 
			
		||||
import { MonacoLanguageClient, ErrorAction, CloseAction, createConnection } from "@codingame/monaco-languageclient";
 | 
			
		||||
import Router from "next/router";
 | 
			
		||||
import normalizeUrl from "normalize-url";
 | 
			
		||||
import ReconnectingWebSocket from "reconnecting-websocket";
 | 
			
		||||
 | 
			
		||||
@@ -14,11 +13,7 @@ export function createLanguageClient(connection: MessageConnection): MonacoLangu
 | 
			
		||||
      errorHandler: {
 | 
			
		||||
        error: () => ErrorAction.Continue,
 | 
			
		||||
        closed: () => {
 | 
			
		||||
          if (Router.pathname.includes('/develop')) {
 | 
			
		||||
            return CloseAction.Restart
 | 
			
		||||
          } else {
 | 
			
		||||
            return CloseAction.DoNotRestart
 | 
			
		||||
          }
 | 
			
		||||
          return CloseAction.DoNotRestart
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										24
									
								
								utils/object.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								utils/object.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
export const deepEqual = (object1: any, object2: any) => {
 | 
			
		||||
    if (!isObject(object1) || !isObject(object2)) return object1 === object2
 | 
			
		||||
 | 
			
		||||
    const keys1 = Object.keys(object1);
 | 
			
		||||
    const keys2 = Object.keys(object2);
 | 
			
		||||
    if (keys1.length !== keys2.length) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
    for (const key of keys1) {
 | 
			
		||||
        const val1 = object1[key];
 | 
			
		||||
        const val2 = object2[key];
 | 
			
		||||
        const areObjects = isObject(val1) && isObject(val2);
 | 
			
		||||
        if (
 | 
			
		||||
            areObjects && !deepEqual(val1, val2) ||
 | 
			
		||||
            !areObjects && val1 !== val2
 | 
			
		||||
        ) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
export const isObject = (object: any) => {
 | 
			
		||||
    return object != null && typeof object === 'object';
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								utils/schema.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								utils/schema.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
export const extractSchemaProps = <O extends object>(obj: O) => 
 | 
			
		||||
    Object.entries(obj).reduce((prev, [key, val]) => {
 | 
			
		||||
        const typeOf = <T>(arg: T) =>
 | 
			
		||||
            arg instanceof Array
 | 
			
		||||
                ? "array"
 | 
			
		||||
                : arg === null
 | 
			
		||||
                    ? "undefined"
 | 
			
		||||
                    : typeof arg;
 | 
			
		||||
 | 
			
		||||
        const value = (typeOf(val) === "object" && '$type' in val && '$value' in val) ? val?.$value : val;
 | 
			
		||||
        const type = typeOf(value);
 | 
			
		||||
 | 
			
		||||
        let schema: any = {
 | 
			
		||||
            title: key,
 | 
			
		||||
            type,
 | 
			
		||||
            default: value,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (typeOf(value) === 'array') {
 | 
			
		||||
            const item = value[0] // TODO merge other item schema's into one
 | 
			
		||||
            if (typeOf(item) !== 'object') {
 | 
			
		||||
                schema.items = {
 | 
			
		||||
                    type: 'object',
 | 
			
		||||
                    properties: extractSchemaProps(item),
 | 
			
		||||
                    default: item
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            // TODO support primitive-value arrays
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (typeOf(value) === "object") {
 | 
			
		||||
            schema.properties = extractSchemaProps(value)
 | 
			
		||||
        }
 | 
			
		||||
        return {
 | 
			
		||||
            ...prev,
 | 
			
		||||
            [key]: schema,
 | 
			
		||||
        };
 | 
			
		||||
    }, {} as any);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										78
									
								
								utils/setHook.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								utils/setHook.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
import { getTags } from './comment-parser';
 | 
			
		||||
import { tts, TTS } from './hookOnCalculator';
 | 
			
		||||
 | 
			
		||||
export const transactionOptions = Object.keys(tts).map(key => ({
 | 
			
		||||
    label: key,
 | 
			
		||||
    value: key as keyof TTS,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export type SetHookData = {
 | 
			
		||||
    Invoke: {
 | 
			
		||||
        value: keyof TTS;
 | 
			
		||||
        label: string;
 | 
			
		||||
    }[];
 | 
			
		||||
    Fee: string;
 | 
			
		||||
    HookNamespace: string;
 | 
			
		||||
    HookParameters: {
 | 
			
		||||
        HookParameter: {
 | 
			
		||||
            HookParameterName: string;
 | 
			
		||||
            HookParameterValue: string;
 | 
			
		||||
        };
 | 
			
		||||
        $metaData?: any;
 | 
			
		||||
    }[];
 | 
			
		||||
    // HookGrants: {
 | 
			
		||||
    //   HookGrant: {
 | 
			
		||||
    //     Authorize: string;
 | 
			
		||||
    //     HookHash: string;
 | 
			
		||||
    //   };
 | 
			
		||||
    // }[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export const getParameters = (content?: string) => {
 | 
			
		||||
    const fieldTags = ["field", "param", "arg", "argument"];
 | 
			
		||||
    const tags = getTags(content)
 | 
			
		||||
        .filter(tag => fieldTags.includes(tag.tag))
 | 
			
		||||
        .filter(tag => !!tag.name);
 | 
			
		||||
 | 
			
		||||
    const paramters: SetHookData["HookParameters"] = tags.map(tag => ({
 | 
			
		||||
        HookParameter: {
 | 
			
		||||
            HookParameterName: tag.name,
 | 
			
		||||
            HookParameterValue: tag.default || "",
 | 
			
		||||
        },
 | 
			
		||||
        $metaData: {
 | 
			
		||||
            description: tag.description,
 | 
			
		||||
            required: !tag.optional
 | 
			
		||||
        },
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    return paramters;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getInvokeOptions = (content?: string) => {
 | 
			
		||||
    const invokeTags = ["invoke", "invoke-on"];
 | 
			
		||||
 | 
			
		||||
    const options = getTags(content)
 | 
			
		||||
        .filter(tag => invokeTags.includes(tag.tag))
 | 
			
		||||
        .reduce((cumm, curr) => {
 | 
			
		||||
            const combined = curr.type || `${curr.name} ${curr.description}`
 | 
			
		||||
            const opts = combined.split(' ')
 | 
			
		||||
 | 
			
		||||
            return cumm.concat(opts as any)
 | 
			
		||||
        }, [] as (keyof TTS)[])
 | 
			
		||||
        .filter(opt => Object.keys(tts).includes(opt))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    const invokeOptions: SetHookData['Invoke'] = options.map(opt => ({
 | 
			
		||||
        label: opt,
 | 
			
		||||
        value: opt
 | 
			
		||||
    }))
 | 
			
		||||
 | 
			
		||||
    // default
 | 
			
		||||
    if (!invokeOptions.length) {
 | 
			
		||||
        const payment = transactionOptions.find(tx => tx.value === "ttPAYMENT")
 | 
			
		||||
        if (payment) return [payment]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return invokeOptions;
 | 
			
		||||
};
 | 
			
		||||
@@ -3,6 +3,7 @@ import hooksAccountConvBufLen from "./md/hooks-account-conv-buf-len.md";
 | 
			
		||||
import hooksAccountConvPure from "./md/hooks-account-conv-pure.md";
 | 
			
		||||
import hooksArrayBufLen from "./md/hooks-array-buf-len.md";
 | 
			
		||||
import hooksBurdenPrereq from "./md/hooks-burden-prereq.md";
 | 
			
		||||
import hooksControlStringArg from "./md/hooks-control-string-arg.md";
 | 
			
		||||
import hooksDetailBufLen from "./md/hooks-detail-buf-len.md";
 | 
			
		||||
import hooksDetailPrereq from "./md/hooks-detail-prereq.md";
 | 
			
		||||
import hooksEmitBufLen from "./md/hooks-emit-buf-len.md";
 | 
			
		||||
@@ -25,17 +26,22 @@ import hooksGuardInFor from "./md/hooks-guard-in-for.md";
 | 
			
		||||
import hooksGuardInWhile from "./md/hooks-guard-in-while.md";
 | 
			
		||||
import hooksHashBufLen from "./md/hooks-hash-buf-len.md";
 | 
			
		||||
import hooksKeyletBufLen from "./md/hooks-keylet-buf-len.md";
 | 
			
		||||
import hooksParamBufLen from "./md/hooks-param-buf-len.md";
 | 
			
		||||
import hooksParamSetBufLen from "./md/hooks-param-set-buf-len.md";
 | 
			
		||||
import hooksRaddrConvBufLen from "./md/hooks-raddr-conv-buf-len.md";
 | 
			
		||||
import hooksRaddrConvPure from "./md/hooks-raddr-conv-pure.md";
 | 
			
		||||
import hooksReleaseDefine from "./md/hooks-release-define.md";
 | 
			
		||||
import hooksReserveLimit from "./md/hooks-reserve-limit.md";
 | 
			
		||||
import hooksSlotHashBufLen from "./md/hooks-slot-hash-buf-len.md";
 | 
			
		||||
import hooksSlotKeyletBufLen from "./md/hooks-slot-keylet-buf-len.md";
 | 
			
		||||
import hooksSlotLimit from "./md/hooks-slot-limit.md";
 | 
			
		||||
import hooksSlotSubLimit from "./md/hooks-slot-sub-limit.md";
 | 
			
		||||
import hooksSlotTypeLimit from "./md/hooks-slot-type-limit.md";
 | 
			
		||||
import hooksSkipHashBufLen from "./md/hooks-skip-hash-buf-len.md";
 | 
			
		||||
import hooksStateBufLen from "./md/hooks-state-buf-len.md";
 | 
			
		||||
import hooksTransactionHashBufLen from "./md/hooks-transaction-hash-buf-len.md";
 | 
			
		||||
import hooksTransactionSlotLimit from "./md/hooks-transaction-slot-limit.md";
 | 
			
		||||
import hooksTrivialCbak from "./md/hooks-trivial-cbak.md";
 | 
			
		||||
import hooksValidateBufLen from "./md/hooks-validate-buf-len.md";
 | 
			
		||||
import hooksVerifyBufLen from "./md/hooks-verify-buf-len.md";
 | 
			
		||||
 | 
			
		||||
@@ -47,6 +53,7 @@ const docs: { [key: string]: string; } = {
 | 
			
		||||
  "hooks-account-conv-pure": hooksAccountConvPure,
 | 
			
		||||
  "hooks-array-buf-len": hooksArrayBufLen,
 | 
			
		||||
  "hooks-burden-prereq": hooksBurdenPrereq,
 | 
			
		||||
  "hooks-control-string-arg": hooksControlStringArg,
 | 
			
		||||
  "hooks-detail-buf-len": hooksDetailBufLen,
 | 
			
		||||
  "hooks-detail-prereq": hooksDetailPrereq,
 | 
			
		||||
  "hooks-emit-buf-len": hooksEmitBufLen,
 | 
			
		||||
@@ -69,17 +76,22 @@ const docs: { [key: string]: string; } = {
 | 
			
		||||
  "hooks-guard-in-while": hooksGuardInWhile,
 | 
			
		||||
  "hooks-hash-buf-len": hooksHashBufLen,
 | 
			
		||||
  "hooks-keylet-buf-len": hooksKeyletBufLen,
 | 
			
		||||
  "hooks-param-buf-len": hooksParamBufLen,
 | 
			
		||||
  "hooks-param-set-buf-len": hooksParamSetBufLen,
 | 
			
		||||
  "hooks-raddr-conv-buf-len": hooksRaddrConvBufLen,
 | 
			
		||||
  "hooks-raddr-conv-pure": hooksRaddrConvPure,
 | 
			
		||||
  "hooks-release-define": hooksReleaseDefine,
 | 
			
		||||
  "hooks-reserve-limit": hooksReserveLimit,
 | 
			
		||||
  "hooks-slot-hash-buf-len": hooksSlotHashBufLen,
 | 
			
		||||
  "hooks-slot-keylet-buf-len": hooksSlotKeyletBufLen,
 | 
			
		||||
  "hooks-slot-limit": hooksSlotLimit,
 | 
			
		||||
  "hooks-slot-sub-limit": hooksSlotSubLimit,
 | 
			
		||||
  "hooks-slot-type-limit": hooksSlotTypeLimit,
 | 
			
		||||
  "hooks-skip-hash-buf-len": hooksSkipHashBufLen,
 | 
			
		||||
  "hooks-state-buf-len": hooksStateBufLen,
 | 
			
		||||
  "hooks-transaction-hash-buf-len": hooksTransactionHashBufLen,
 | 
			
		||||
  "hooks-transaction-slot-limit": hooksTransactionSlotLimit,
 | 
			
		||||
  "hooks-trivial-cbak": hooksTrivialCbak,
 | 
			
		||||
  "hooks-validate-buf-len": hooksValidateBufLen,
 | 
			
		||||
  "hooks-verify-buf-len": hooksVerifyBufLen,
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
# hooks-account-buf-len
 | 
			
		||||
 | 
			
		||||
Function `hook_account` has fixed-size account ID output.
 | 
			
		||||
Function [hook_account](https://xrpl-hooks.readme.io/v2.0/reference/hook_account) has fixed-size account ID output.
 | 
			
		||||
 | 
			
		||||
This check warns about too-small size of its output buffer (if it's
 | 
			
		||||
specified by a constant - variable parameter is ignored).
 | 
			
		||||
This check warns about too-small size of its output buffer (if it's specified by a constant - variable parameter is ignored).
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,5 @@
 | 
			
		||||
# hooks-account-conv-buf-len
 | 
			
		||||
 | 
			
		||||
Function `util_raddr` has fixed-size account ID input.
 | 
			
		||||
Function [util_raddr](https://xrpl-hooks.readme.io/v2.0/reference/util_raddr) has fixed-size account ID input.
 | 
			
		||||
 | 
			
		||||
This check warns unless the correct size is passed in the input size
 | 
			
		||||
parameter (if it's specified by a constant - variable parameter is
 | 
			
		||||
ignored).
 | 
			
		||||
This check warns unless the correct size is passed in the input size parameter (if it's specified by a constant - variable parameter is ignored).
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,5 @@
 | 
			
		||||
# hooks-account-conv-pure
 | 
			
		||||
 | 
			
		||||
Hooks identify accounts by the 20 byte account ID, which can be
 | 
			
		||||
converted to an raddr using the `util_raddr` function. If the account
 | 
			
		||||
ID never changes, a more efficient way to do this is precompute the
 | 
			
		||||
raddr from the account ID.
 | 
			
		||||
Hooks identify accounts by the 20 byte account ID, which can be converted to an raddr using the [util_raddr](https://xrpl-hooks.readme.io/v2.0/reference/util_raddr) function. If the account ID never changes, a more efficient way to do this is precompute the raddr from the account ID.
 | 
			
		||||
 | 
			
		||||
This check warns about calls of `util_raddr` with constant input and
 | 
			
		||||
proposes to add a tracing statement showing the computed value (so
 | 
			
		||||
that the user can use it to replace the call).
 | 
			
		||||
This check warns about calls of `util_raddr` with constant input and proposes to add a tracing statement showing the computed value (so that the user can use it to replace the call).
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,7 @@
 | 
			
		||||
# hooks-array-buf-len
 | 
			
		||||
 | 
			
		||||
Hook API `sto_subarray` requires non-empty input buffer and takes a
 | 
			
		||||
parameter specifying the array index, whose value is limited - the
 | 
			
		||||
sought object cannot be found if the limit is exceeded.
 | 
			
		||||
Hook API [sto_subarray](https://xrpl-hooks.readme.io/v2.0/reference/sto_subarray) requires non-empty input buffer and takes a parameter specifying the array index, whose value is limited - the sought object cannot be found if the limit is exceeded.
 | 
			
		||||
 | 
			
		||||
This check warns about empty input as well as too-large values of the
 | 
			
		||||
index specified in calls to `sto_subarray` (if they're specified by
 | 
			
		||||
constants - variable parameters are ignored).
 | 
			
		||||
This check warns about empty input as well as too-large values of the index specified in calls to `sto_subarray` (if they're specified by constants - variable parameters are ignored).
 | 
			
		||||
 | 
			
		||||
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/serialized-objects)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,3 @@
 | 
			
		||||
# hooks-burden-prereq
 | 
			
		||||
 | 
			
		||||
Hook API `etxn_burden` computes transaction burden, based on (i.a.)
 | 
			
		||||
the number of reserved transactions, so a call to it must be preceded
 | 
			
		||||
by a call to `etxn_reserve`.
 | 
			
		||||
Hook API [etxn_burden](https://xrpl-hooks.readme.io/v2.0/reference/etxn_burden) computes transaction burden, based on (i.a.) the number of reserved transactions, so a call to it must be preceded by a call to [etxn_reserve](https://xrpl-hooks.readme.io/v2.0/reference/etxn_reserve).
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								xrpl-hooks-docs/md/hooks-control-string-arg.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								xrpl-hooks-docs/md/hooks-control-string-arg.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
# hooks-control-string-arg
 | 
			
		||||
 | 
			
		||||
Functions [accept](https://xrpl-hooks.readme.io/v2.0/reference/accept) and [rollback](https://xrpl-hooks.readme.io/v2.0/reference/rollback) take an optional string buffer stored outside the hook as its result message. This is useful for debugging but takes up space.
 | 
			
		||||
 | 
			
		||||
For a release version, this check warns about constant strings passed to `accept` and `rollback`.
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
# hooks-detail-buf-len
 | 
			
		||||
 | 
			
		||||
Function `etxn_details` has fixed-size sfEmitDetails output.
 | 
			
		||||
Function [etxn_details](https://xrpl-hooks.readme.io/v2.0/reference/etxn_details) has fixed-size sfEmitDetails output.
 | 
			
		||||
 | 
			
		||||
This check warns about too-small size of its output buffer (if it's
 | 
			
		||||
specified by a constant - variable parameter is ignored).
 | 
			
		||||
This check warns about too-small size of its output buffer (if it's specified by a constant - variable parameter is ignored).
 | 
			
		||||
 | 
			
		||||
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/emitted-transactions)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
# hooks-detail-prereq
 | 
			
		||||
 | 
			
		||||
Hook API `etxn_details` serializes emit details, based on (i.a.) the
 | 
			
		||||
number of reserved transactions, so a call to it must be preceded by a
 | 
			
		||||
call to `etxn_reserve`.
 | 
			
		||||
Hook API [etxn_details](https://xrpl-hooks.readme.io/v2.0/reference/etxn_details) serializes emit details, based on (i.a.) the number of reserved transactions, so a call to it must be preceded by a call to [etxn_reserve](https://xrpl-hooks.readme.io/v2.0/reference/etxn_reserve).
 | 
			
		||||
 | 
			
		||||
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/emitted-transactions)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
# hooks-emit-buf-len
 | 
			
		||||
 | 
			
		||||
Function `emit` has fixed-size transaction hash output.
 | 
			
		||||
Function [emit](https://xrpl-hooks.readme.io/v2.0/reference/emit) has fixed-size transaction hash output.
 | 
			
		||||
 | 
			
		||||
This check warns about too-small size of its output buffer (if it's
 | 
			
		||||
specified by a constant - variable parameter is ignored).
 | 
			
		||||
This check warns about too-small size of its output buffer (if it's specified by a constant - variable parameter is ignored).
 | 
			
		||||
 | 
			
		||||
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/emitted-transactions)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
# hooks-emit-prereq
 | 
			
		||||
 | 
			
		||||
Before emitting a transaction using `emit` Hook API, a hook must set a
 | 
			
		||||
maximal count of transactions it plans to emit, by calling
 | 
			
		||||
`etxn_reserve`.
 | 
			
		||||
Before emitting a transaction using [emit](https://xrpl-hooks.readme.io/v2.0/reference/emit) Hook API, a hook must set a maximal count of transactions it plans to emit, by calling [etxn_reserve](https://xrpl-hooks.readme.io/v2.0/reference/etxn_reserve).
 | 
			
		||||
 | 
			
		||||
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/emitted-transactions)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
# hooks-entry-point-recursion
 | 
			
		||||
 | 
			
		||||
Recursive calls are disallowed in the implementation of hook entry
 | 
			
		||||
points.
 | 
			
		||||
Recursive calls are disallowed in the implementation of hook entry points.
 | 
			
		||||
 | 
			
		||||
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/loops-and-guarding#no-recursion)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
# hooks-entry-points-neg
 | 
			
		||||
 | 
			
		||||
Shows error on function definitions with unexpected (that is, neither
 | 
			
		||||
`hook` nor `cbak`) names.
 | 
			
		||||
Shows error on function definitions with unexpected (that is, neither `hook` nor `cbak`) names.
 | 
			
		||||
 | 
			
		||||
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/compiling-hooks#constraints)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
# hooks-entry-points
 | 
			
		||||
 | 
			
		||||
A Hook always implements and exports exactly two functions: `cbak` and
 | 
			
		||||
`hook`.
 | 
			
		||||
A Hook always implements and exports a [hook](https://xrpl-hooks.readme.io/v2.0/reference/hook) function.
 | 
			
		||||
 | 
			
		||||
This check shows error on translation units that do not have them.
 | 
			
		||||
This check shows error on translation units that do not have it.
 | 
			
		||||
 | 
			
		||||
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/compiling-hooks)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
# hooks-fee-prereq
 | 
			
		||||
 | 
			
		||||
Hook API `etxn_fee_base` estimates a transaction fee, based on (i.a.)
 | 
			
		||||
the number of reserved transactions, so a call to it must be preceded
 | 
			
		||||
by a call to `etxn_reserve`.
 | 
			
		||||
Hook API [etxn_fee_base](https://xrpl-hooks.readme.io/v2.0/reference/etxn_fee_base) estimates a transaction fee, based on (i.a.) the number of reserved transactions, so a call to it must be preceded by a call to [etxn_reserve](https://xrpl-hooks.readme.io/v2.0/reference/etxn_reserve).
 | 
			
		||||
 | 
			
		||||
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/hook-fees)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,7 @@
 | 
			
		||||
# hooks-field-add-buf-len
 | 
			
		||||
 | 
			
		||||
Emplacing a new field into STObject by calling `sto_emplace` requires
 | 
			
		||||
enough space to serialize the new STObject into; the API also limits
 | 
			
		||||
sizes of the old object and field.
 | 
			
		||||
Emplacing a new field into STObject by calling [sto_emplace](https://xrpl-hooks.readme.io/v2.0/reference/sto_emplace) requires enough space to serialize the new STObject into; the API also limits sizes of the old object and field.
 | 
			
		||||
 | 
			
		||||
This check warns about insufficient output buffer space as well as
 | 
			
		||||
too-large values of the inputs in calls to `sto_emplace` (if they're
 | 
			
		||||
specified by constants - variable parameters are ignored).
 | 
			
		||||
This check warns about insufficient output buffer space as well as too-large values of the inputs in calls to `sto_emplace` (if they're specified by constants - variable parameters are ignored).
 | 
			
		||||
 | 
			
		||||
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/serialized-objects)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
# hooks-field-buf-len
 | 
			
		||||
 | 
			
		||||
Hook API `sto_subfield` requires non-empty input buffer.
 | 
			
		||||
Hook API [sto_subfield](https://xrpl-hooks.readme.io/v2.0/reference/sto_subfield) requires non-empty input buffer.
 | 
			
		||||
 | 
			
		||||
This check warns about empty input in calls to `sto_subfield` (if it's
 | 
			
		||||
specified by a constant - variable parameter is ignored).
 | 
			
		||||
This check warns about empty input in calls to `sto_subfield` (if it's specified by a constant - variable parameter is ignored).
 | 
			
		||||
 | 
			
		||||
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/serialized-objects)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,7 @@
 | 
			
		||||
# hooks-field-del-buf-len
 | 
			
		||||
 | 
			
		||||
Erasing a field from STObject by calling `sto_erase` requires enough
 | 
			
		||||
space to serialize the new STObject into; the API also limits size of
 | 
			
		||||
the old object.
 | 
			
		||||
Erasing a field from STObject by calling [sto_erase](https://xrpl-hooks.readme.io/v2.0/reference/sto_erase) requires enough space to serialize the new STObject into; the API also limits size of the old object.
 | 
			
		||||
 | 
			
		||||
This check warns about insufficient output buffer space as well as
 | 
			
		||||
too-large value of the input STObject in calls to `sto_erase` (if
 | 
			
		||||
they're specified by constants - variable parameters are ignored).
 | 
			
		||||
This check warns about insufficient output buffer space as well as too-large value of the input STObject in calls to `sto_erase` (if they're specified by constants - variable parameters are ignored).
 | 
			
		||||
 | 
			
		||||
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/serialized-objects)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,8 @@
 | 
			
		||||
# hooks-float-arith-pure
 | 
			
		||||
 | 
			
		||||
Hooks can compute floating-point values in XFL format by calling
 | 
			
		||||
functions `float_multiply`, `float_mulratio`, `float_negate`,
 | 
			
		||||
`float_sum`, `float_invert` and `float_divide` and access their
 | 
			
		||||
constituent parts by calling `float_exponent`, `float_mantissa` and
 | 
			
		||||
`float_sign`. If the inputs of the computation never change, a more
 | 
			
		||||
efficient way to do this is to precompute it.
 | 
			
		||||
Hooks can compute floating-point values in XFL format by calling functions [float_multiply](https://xrpl-hooks.readme.io/v2.0/reference/float_multiply), [float_mulratio](https://xrpl-hooks.readme.io/v2.0/reference/float_mulratio), [float_negate](https://xrpl-hooks.readme.io/v2.0/reference/float_negate), [float_sum](https://xrpl-hooks.readme.io/v2.0/reference/float_sum), [float_invert](https://xrpl-hooks.readme.io/v2.0/reference/float_invert) and [float_divide](https://xrpl-hooks.readme.io/v2.0/reference/float_divide) and access their constituent parts by calling [float_exponent](https://xrpl-hooks.readme.io/v2.0/reference/float_exponent), [float_mantissa](https://xrpl-hooks.readme.io/v2.0/reference/float_mantissa) and [float_sign](https://xrpl-hooks.readme.io/v2.0/reference/float_sign). If the inputs of the computation never change, a more efficient way to do this is to precompute it.
 | 
			
		||||
 | 
			
		||||
This check warns about calls of the aforementioned functions with constant inputs and in simple cases proposes to add a tracing statement showing the computed value (so that the user can use it to replace the call). It also checks that the divisor passed to `float_divide`, `float_mulratio` and `float_invert` is not 0 (if it's specified by a constant - variable parameters are ignored).
 | 
			
		||||
 | 
			
		||||
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/floating-point-numbers-xfl)
 | 
			
		||||
 | 
			
		||||
This check warns about calls of the aforementioned functions with
 | 
			
		||||
constant inputs and in simple cases proposes to add a tracing
 | 
			
		||||
statement showing the computed value (so that the user can use it to
 | 
			
		||||
replace the call). It also checks that the divisor passed to
 | 
			
		||||
`float_divide`, `float_mulratio` and `float_invert` is not 0 (if it's
 | 
			
		||||
specified by a constant - variable parameters are ignored).
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,7 @@
 | 
			
		||||
# hooks-float-compare-pure
 | 
			
		||||
 | 
			
		||||
Hooks can compare floating-point values in XFL format by calling the
 | 
			
		||||
`float_compare` function. If the inputs of the comparison never
 | 
			
		||||
change, its result is fixed and the function need not be called.
 | 
			
		||||
Hooks can compare floating-point values in XFL format by calling the [float_compare](https://xrpl-hooks.readme.io/v2.0/reference/float_compare) function. If the inputs of the comparison never change, its result is fixed and the function need not be called.
 | 
			
		||||
 | 
			
		||||
This check warns about calls of `float_compare` with constant inputs
 | 
			
		||||
as well as invalid values of the comparison mode parameter (if it's
 | 
			
		||||
specified by a constant - variable parameter is ignored).
 | 
			
		||||
This check warns about calls of `float_compare` with constant inputs as well as invalid values of the comparison mode parameter (if it's specified by a constant - variable parameter is ignored).
 | 
			
		||||
 | 
			
		||||
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/floating-point-numbers-xfl)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,7 @@
 | 
			
		||||
# hooks-float-int-pure
 | 
			
		||||
 | 
			
		||||
Hooks can convert floating-point values in XFL format to integers by
 | 
			
		||||
calling the `float_int` function. If the inputs of this function never
 | 
			
		||||
change, a more efficient way to do this is to precompute the integer
 | 
			
		||||
value.
 | 
			
		||||
Hooks can convert floating-point values in XFL format to integers by calling the [float_int](https://xrpl-hooks.readme.io/v2.0/reference/float_int) function. If the inputs of this function never change, a more efficient way to do this is to precompute the integer value.
 | 
			
		||||
 | 
			
		||||
This check warns about calls of `float_int` with constant inputs as
 | 
			
		||||
well as invalid values of the decimal places parameter (if it's
 | 
			
		||||
specified by a constant - variable parameter is ignored).
 | 
			
		||||
This check warns about calls of `float_int` with constant inputs as well as invalid values of the decimal places parameter (if it's specified by a constant - variable parameter is ignored).
 | 
			
		||||
 | 
			
		||||
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/floating-point-numbers-xfl)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,7 @@
 | 
			
		||||
# hooks-float-manip-pure
 | 
			
		||||
 | 
			
		||||
Hooks can directly manipulate floating-point values in XFL format by
 | 
			
		||||
calling functions `float_exponent_set`, `float_mantissa_set` and
 | 
			
		||||
`float_sign_set`. If the inputs of the update never change, a more
 | 
			
		||||
efficient way to do this is to precompute it.
 | 
			
		||||
Hooks can directly manipulate floating-point values in XFL format by calling functions [float_exponent_set](https://xrpl-hooks.readme.io/v2.0/reference/float_exponent_set), [float_mantissa_set](https://xrpl-hooks.readme.io/v2.0/reference/float_mantissa_set) and [float_sign_set](https://xrpl-hooks.readme.io/v2.0/reference/float_sign_set). If the inputs of the update never change, a more efficient way to do this is to precompute it.
 | 
			
		||||
 | 
			
		||||
This check warns about calls of the aforementioned functions with
 | 
			
		||||
constant inputs and in simple cases proposes to add a tracing
 | 
			
		||||
statement showing the computed value (so that the user can use it to
 | 
			
		||||
replace the call). It also checks documented bounds of the second
 | 
			
		||||
parameter of these functions (if it's specified by a constant -
 | 
			
		||||
variable parameter is ignored).
 | 
			
		||||
This check warns about calls of the aforementioned functions with constant inputs and in simple cases proposes to add a tracing statement showing the computed value (so that the user can use it to replace the call). It also checks documented bounds of the second parameter of these functions (if it's specified by a constant - variable parameter is ignored).
 | 
			
		||||
 | 
			
		||||
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/floating-point-numbers-xfl)
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user