Compare commits

...

134 Commits

Author SHA1 Message Date
muzam1l
d05180d148 Fix array fields. 2022-07-15 14:49:54 +05:30
muzamil
bfaa6be17d Fix binary codec (#250)
* TokenID -> NFTokenID

* update xrpl-accountlib

* fixup patch

* add missing transaction types

* fix: repositories

* fix: update @types/react, move middleware

* revert yarn.lock

* revert

* revert: yarn.lock

* fix: transactions

* update NFTokenCancelOffer

* add missing fee

* add: nft transaction type + recalculate `start`

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

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

* Remove unused imports

* updated gist IDs for xrplfgists

* Add macro.h to apiheaderfiles

* Update headers

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

* added hooks-trivial-cbak

* updated hooks-hash-buf-len: nonce => etxn_nonce + ledger_nonce
2022-05-30 12:49:19 +02:00
muzam1l
da9986eb66 Remove unnecessary console logs 2022-05-27 22:46:15 +05:30
muzam1l
a21350770e Fee hints in transactions. 2022-05-27 16:44:01 +05:30
Valtteri Karesto
49dfd43220 Merge pull request #193 from XRPLF/feat/fee-estimates
Feat/fee estimates
2022-05-27 12:23:50 +03:00
muzamil
ca46707bb5 Merge pull request #195 from XRPLF/feat/tx_hash_link
Show tx hash instead of server ledger index in deploy log.
2022-05-26 15:28:31 +05:30
muzam1l
704ebe4b92 Show tx hash instead of server ledger index in deploy log. 2022-05-26 14:55:24 +05:30
49 changed files with 2569 additions and 1563 deletions

View File

@@ -1,4 +1,5 @@
NEXTAUTH_URL=https://example.com NEXTAUTH_URL=https://example.com
NEXTAUTH_SECRET="1234"
GITHUB_SECRET="" GITHUB_SECRET=""
GITHUB_ID="" GITHUB_ID=""
NEXT_PUBLIC_COMPILE_API_ENDPOINT="http://localhost:9000/api/build" NEXT_PUBLIC_COMPILE_API_ENDPOINT="http://localhost:9000/api/build"

1
.gitignore vendored
View File

@@ -32,3 +32,4 @@ yarn-error.log*
# vercel # vercel
.vercel .vercel
.vscode

View File

@@ -108,3 +108,5 @@ To learn more about Next.js, take a look at the following resources:
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

View File

@@ -31,6 +31,7 @@ import transactionsData from "../content/transactions.json";
import { SetHookDialog } from "./SetHookDialog"; import { SetHookDialog } from "./SetHookDialog";
import { addFunds } from "../state/actions/addFaucetAccount"; import { addFunds } from "../state/actions/addFaucetAccount";
import { deleteHook } from "../state/actions/deployHook"; import { deleteHook } from "../state/actions/deployHook";
import { capitalize } from "../utils/helpers";
export const AccountDialog = ({ export const AccountDialog = ({
activeAccountAddress, activeAccountAddress,
@@ -42,12 +43,12 @@ export const AccountDialog = ({
const snap = useSnapshot(state); const snap = useSnapshot(state);
const [showSecret, setShowSecret] = useState(false); const [showSecret, setShowSecret] = useState(false);
const activeAccount = snap.accounts.find( const activeAccount = snap.accounts.find(
(account) => account.address === activeAccountAddress account => account.address === activeAccountAddress
); );
return ( return (
<Dialog <Dialog
open={Boolean(activeAccountAddress)} open={Boolean(activeAccountAddress)}
onOpenChange={(open) => { onOpenChange={open => {
setShowSecret(false); setShowSecret(false);
!open && setActiveAccountAddress(null); !open && setActiveAccountAddress(null);
}} }}
@@ -99,7 +100,7 @@ export const AccountDialog = ({
tabIndex={-1} tabIndex={-1}
onClick={() => { onClick={() => {
const index = state.accounts.findIndex( const index = state.accounts.findIndex(
(acc) => acc.address === activeAccount?.address acc => acc.address === activeAccount?.address
); );
state.accounts.splice(index, 1); state.accounts.splice(index, 1);
}} }}
@@ -116,9 +117,16 @@ export const AccountDialog = ({
<Text <Text
css={{ css={{
fontFamily: "$monospace", fontFamily: "$monospace",
a: { "&:hover": { textDecoration: "underline" } },
}} }}
> >
{activeAccount?.address} <a
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
target="_blank"
rel="noopener noreferrer"
>
{activeAccount?.address}
</a>
</Text> </Text>
</Flex> </Flex>
<Flex css={{ marginLeft: "auto", color: "$mauve12" }}> <Flex css={{ marginLeft: "auto", color: "$mauve12" }}>
@@ -158,7 +166,7 @@ export const AccountDialog = ({
}} }}
ghost ghost
size="xs" size="xs"
onClick={() => setShowSecret((curr) => !curr)} onClick={() => setShowSecret(curr => !curr)}
> >
{showSecret ? "Hide" : "Show"} {showSecret ? "Hide" : "Show"}
</Button> </Button>
@@ -215,7 +223,11 @@ export const AccountDialog = ({
</Button> </Button>
</Text> </Text>
</Flex> </Flex>
<Flex css={{ marginLeft: "auto" }}> <Flex
css={{
marginLeft: "auto",
}}
>
<a <a
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`} href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
target="_blank" target="_blank"
@@ -237,10 +249,22 @@ export const AccountDialog = ({
<Text <Text
css={{ css={{
fontFamily: "$monospace", fontFamily: "$monospace",
a: { "&:hover": { textDecoration: "underline" } },
}} }}
> >
{activeAccount && activeAccount.hooks.length > 0 {activeAccount && activeAccount.hooks.length > 0
? activeAccount.hooks.map((i) => truncate(i, 12)).join(",") ? activeAccount.hooks.map(i => {
return (
<a
key={i}
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${i}`}
target="_blank"
rel="noopener noreferrer"
>
{truncate(i, 12)}
</a>
);
})
: ""} : ""}
</Text> </Text>
</Flex> </Flex>
@@ -278,7 +302,7 @@ interface AccountProps {
showHookStats?: boolean; showHookStats?: boolean;
} }
const Accounts: FC<AccountProps> = (props) => { const Accounts: FC<AccountProps> = props => {
const snap = useSnapshot(state); const snap = useSnapshot(state);
const [activeAccountAddress, setActiveAccountAddress] = useState< const [activeAccountAddress, setActiveAccountAddress] = useState<
string | null string | null
@@ -286,7 +310,7 @@ const Accounts: FC<AccountProps> = (props) => {
useEffect(() => { useEffect(() => {
const fetchAccInfo = async () => { const fetchAccInfo = async () => {
if (snap.clientStatus === "online") { if (snap.clientStatus === "online") {
const requests = snap.accounts.map((acc) => const requests = snap.accounts.map(acc =>
snap.client?.send({ snap.client?.send({
id: `hooks-builder-req-info-${acc.address}`, id: `hooks-builder-req-info-${acc.address}`,
command: "account_info", command: "account_info",
@@ -299,7 +323,7 @@ const Accounts: FC<AccountProps> = (props) => {
const balance = res?.account_data?.Balance as string; const balance = res?.account_data?.Balance as string;
const sequence = res?.account_data?.Sequence as number; const sequence = res?.account_data?.Sequence as number;
const accountToUpdate = state.accounts.find( const accountToUpdate = state.accounts.find(
(acc) => acc.address === address acc => acc.address === address
); );
if (accountToUpdate) { if (accountToUpdate) {
accountToUpdate.xrp = balance; accountToUpdate.xrp = balance;
@@ -307,7 +331,7 @@ const Accounts: FC<AccountProps> = (props) => {
accountToUpdate.error = null; accountToUpdate.error = null;
} else { } else {
const oldAccount = state.accounts.find( const oldAccount = state.accounts.find(
(acc) => acc.address === res?.account acc => acc.address === res?.account
); );
if (oldAccount) { if (oldAccount) {
oldAccount.xrp = "0"; oldAccount.xrp = "0";
@@ -318,7 +342,7 @@ const Accounts: FC<AccountProps> = (props) => {
} }
} }
}); });
const objectRequests = snap.accounts.map((acc) => { const objectRequests = snap.accounts.map(acc => {
return snap.client?.send({ return snap.client?.send({
id: `hooks-builder-req-objects-${acc.address}`, id: `hooks-builder-req-objects-${acc.address}`,
command: "account_objects", command: "account_objects",
@@ -329,7 +353,7 @@ const Accounts: FC<AccountProps> = (props) => {
objectResponses.forEach((res: any) => { objectResponses.forEach((res: any) => {
const address = res?.account as string; const address = res?.account as string;
const accountToUpdate = state.accounts.find( const accountToUpdate = state.accounts.find(
(acc) => acc.address === address acc => acc.address === address
); );
if (accountToUpdate) { if (accountToUpdate) {
accountToUpdate.hooks = accountToUpdate.hooks =
@@ -393,9 +417,7 @@ const Accounts: FC<AccountProps> = (props) => {
<Wallet size="15px" /> <Text css={{ lineHeight: 1 }}>Accounts</Text> <Wallet size="15px" /> <Text css={{ lineHeight: 1 }}>Accounts</Text>
</Heading> </Heading>
<Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}> <Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
<Button ghost size="sm" onClick={() => addFaucetAccount(true)}> <ImportAccountDialog type="create" />
Create
</Button>
<ImportAccountDialog /> <ImportAccountDialog />
</Flex> </Flex>
</Flex> </Flex>
@@ -412,7 +434,7 @@ const Accounts: FC<AccountProps> = (props) => {
overflowY: "auto", overflowY: "auto",
}} }}
> >
{snap.accounts.map((account) => ( {snap.accounts.map(account => (
<Flex <Flex
column column
key={account.address + account.name} key={account.address + account.name}
@@ -465,7 +487,7 @@ const Accounts: FC<AccountProps> = (props) => {
{!props.hideDeployBtn && ( {!props.hideDeployBtn && (
<div <div
className="hook-deploy-button" className="hook-deploy-button"
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
}} }}
> >
@@ -491,31 +513,71 @@ const Accounts: FC<AccountProps> = (props) => {
); );
}; };
export const transactionsOptions = transactionsData.map((tx) => ({ export const transactionsOptions = transactionsData.map(tx => ({
value: tx.TransactionType, value: tx.TransactionType,
label: tx.TransactionType, label: tx.TransactionType,
})); }));
const ImportAccountDialog = () => { const ImportAccountDialog = ({
const [value, setValue] = useState(""); 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 ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button ghost size="sm"> <Button ghost size="sm">
Import {btnText}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent aria-describedby={undefined}>
<DialogTitle>Import account</DialogTitle> <DialogTitle css={{ mb: "$4" }}>{title}</DialogTitle>
<DialogDescription> <Flex column>
<Label>Add account secret</Label> <Box css={{ mb: "$2" }}>
<Input <Label>
name="secret" Account name <Text muted>(optional)</Text>
type="password" </Label>
value={value} <Input
onChange={(e) => setValue(e.target.value)} name="name"
/> type="text"
</DialogDescription> autoComplete="off"
autoCapitalize="on"
value={name}
onChange={e => setName(e.target.value)}
/>
</Box>
{type === "import" && (
<Box>
<Label>Account secret</Label>
<Input
required
name="secret"
type="password"
autoComplete="new-password"
value={secret}
onChange={e => setSecret(e.target.value)}
/>
</Box>
)}
</Flex>
<Flex <Flex
css={{ css={{
@@ -528,14 +590,8 @@ const ImportAccountDialog = () => {
<Button outline>Cancel</Button> <Button outline>Cancel</Button>
</DialogClose> </DialogClose>
<DialogClose asChild> <DialogClose asChild>
<Button <Button type="submit" variant="primary" onClick={handleSubmit}>
variant="primary" {title}
onClick={() => {
importAccount(value);
setValue("");
}}
>
Import account
</Button> </Button>
</DialogClose> </DialogClose>
</Flex> </Flex>

View File

@@ -1,5 +1,7 @@
import { useCallback, useEffect } from "react"; import { useEffect } from "react";
import ReconnectingWebSocket, { CloseEvent } from "reconnecting-websocket";
import { proxy, ref, useSnapshot } from "valtio"; import { proxy, ref, useSnapshot } from "valtio";
import { subscribeKey } from "valtio/utils";
import { Select } from "."; import { Select } from ".";
import state, { ILog, transactionsState } from "../state"; import state, { ILog, transactionsState } from "../state";
import { extractJSON } from "../utils/json"; import { extractJSON } from "../utils/json";
@@ -15,7 +17,7 @@ export interface IStreamState {
status: "idle" | "opened" | "closed"; status: "idle" | "opened" | "closed";
statusChangeTimestamp?: number; statusChangeTimestamp?: number;
logs: ILog[]; logs: ILog[];
socket?: WebSocket; socket?: ReconnectingWebSocket;
} }
export const streamState = proxy<IStreamState>({ export const streamState = proxy<IStreamState>({
@@ -24,12 +26,85 @@ export const streamState = proxy<IStreamState>({
logs: [] as ILog[], logs: [] as ILog[],
}); });
const onOpen = (account: ISelect | null) => {
if (!account) {
return;
}
// streamState.logs = [];
streamState.status = "opened";
streamState.statusChangeTimestamp = Date.now();
pushLog(`Debug stream opened for account ${account?.value}`, {
type: "success",
});
};
const onError = () => {
pushLog("Something went wrong! Check your connection and try again.", {
type: "error",
});
};
const onClose = (e: CloseEvent) => {
// 999 = closed websocket connection by switching account
if (e.code !== 4999) {
pushLog(`Connection was closed. [code: ${e.code}]`, {
type: "error",
});
}
streamState.status = "closed";
streamState.statusChangeTimestamp = Date.now();
};
const onMessage = (event: any) => {
// Ping returns just account address, if we get that
// response we don't need to log anything
if (event.data !== streamState.selectedAccount?.value) {
pushLog(event.data);
}
};
let interval: NodeJS.Timer | null = null;
const addListeners = (account: ISelect | null) => {
if (account?.value && streamState.socket?.url.endsWith(account?.value)) {
return;
}
streamState.logs = [];
if (account?.value) {
if (interval) {
clearInterval(interval);
}
if (streamState.socket) {
streamState.socket?.removeEventListener("open", () => onOpen(account));
streamState.socket?.removeEventListener("close", onClose);
streamState.socket?.removeEventListener("error", onError);
streamState.socket?.removeEventListener("message", onMessage);
}
streamState.socket = ref(
new ReconnectingWebSocket(
`wss://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/${account?.value}`
)
);
if (streamState.socket) {
interval = setInterval(() => {
streamState.socket?.send("");
}, 45000);
}
streamState.socket.addEventListener("open", () => onOpen(account));
streamState.socket.addEventListener("close", onClose);
streamState.socket.addEventListener("error", onError);
streamState.socket.addEventListener("message", onMessage);
}
};
subscribeKey(streamState, "selectedAccount", addListeners);
const DebugStream = () => { const DebugStream = () => {
const { selectedAccount, logs, socket } = useSnapshot(streamState); const { selectedAccount, logs } = useSnapshot(streamState);
const { activeHeader: activeTxTab } = useSnapshot(transactionsState); const { activeHeader: activeTxTab } = useSnapshot(transactionsState);
const { accounts } = useSnapshot(state); const { accounts } = useSnapshot(state);
const accountOptions = accounts.map(acc => ({ const accountOptions = accounts.map((acc) => ({
label: acc.name, label: acc.name,
value: acc.address, value: acc.address,
})); }));
@@ -42,117 +117,21 @@ const DebugStream = () => {
options={accountOptions} options={accountOptions}
hideSelectedOptions hideSelectedOptions
value={selectedAccount} 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%" }} css={{ width: "100%" }}
/> />
</> </>
); );
useEffect(() => {
const account = selectedAccount?.value;
if (account && (!socket || !socket.url.endsWith(account))) {
socket?.close();
streamState.socket = ref(
new WebSocket(
`wss://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/${account}`
)
);
} else if (!account && socket) {
socket.close();
streamState.socket = undefined;
}
}, [selectedAccount?.value, socket]);
const onMount = useCallback(async () => {
// deliberately using `proxy` values and not the `useSnapshot` ones to have no dep list
const acc = streamState.selectedAccount;
const status = streamState.status;
if (status === "opened" && acc) {
// fetch the missing ones
try {
const url = `https://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/recent/${acc?.value}`;
// TODO Remove after api sets cors properly
const res = await fetch("/api/proxy", {
method: "POST",
body: JSON.stringify({ url }),
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) return;
const body = await res.json();
if (!body?.logs) return;
const start = streamState.statusChangeTimestamp || 0;
streamState.logs = [];
pushLog(`Debug stream opened for account ${acc.value}`, {
type: "success",
});
const logs = Object.entries(body.logs).filter(([tm]) => +tm >= start);
logs.forEach(([tm, log]) => pushLog(log));
} catch (error) {
console.error(error);
}
}
}, []);
useEffect(() => {
onMount();
}, [onMount]);
useEffect(() => {
const account = selectedAccount?.value;
const socket = streamState.socket;
if (!socket) return;
const onOpen = () => {
streamState.logs = [];
streamState.status = "opened";
streamState.statusChangeTimestamp = Date.now();
pushLog(`Debug stream opened for account ${account}`, {
type: "success",
});
};
const onError = () => {
pushLog("Something went wrong! Check your connection and try again.", {
type: "error",
});
};
const onClose = (e: CloseEvent) => {
pushLog(`Connection was closed. [code: ${e.code}]`, {
type: "error",
});
streamState.selectedAccount = null;
streamState.status = "closed";
streamState.statusChangeTimestamp = Date.now();
};
const onMessage = (event: any) => {
pushLog(event.data);
};
socket.addEventListener("open", onOpen);
socket.addEventListener("close", onClose);
socket.addEventListener("error", onError);
socket.addEventListener("message", onMessage);
return () => {
socket.removeEventListener("open", onOpen);
socket.removeEventListener("close", onClose);
socket.removeEventListener("message", onMessage);
socket.removeEventListener("error", onError);
};
}, [selectedAccount?.value, socket]);
useEffect(() => { useEffect(() => {
const account = transactionsState.transactions.find( const account = transactionsState.transactions.find(
tx => tx.header === activeTxTab (tx) => tx.header === activeTxTab
)?.state.selectedAccount; )?.state.selectedAccount;
if (account && account.value !== streamState.selectedAccount?.value) if (account && account.value !== streamState.selectedAccount?.value)

View File

@@ -1,7 +1,6 @@
import React, { useRef, useState } from "react"; import React, { useState } from "react";
import { useSnapshot, ref } from "valtio"; import { useSnapshot } from "valtio";
import Editor, { loader } from "@monaco-editor/react";
import type monaco from "monaco-editor";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import NextLink from "next/link"; import NextLink from "next/link";
@@ -10,31 +9,25 @@ import filesize from "filesize";
import Box from "./Box"; import Box from "./Box";
import Container from "./Container"; import Container from "./Container";
import dark from "../theme/editor/amy.json";
import light from "../theme/editor/xcode_default.json";
import state from "../state"; import state from "../state";
import wat from "../utils/wat-highlight"; import wat from "../utils/wat-highlight";
import EditorNavigation from "./EditorNavigation"; import EditorNavigation from "./EditorNavigation";
import { Button, Text, Link, Flex } from "."; import { Button, Text, Link, Flex } from ".";
import Monaco from "./Monaco";
loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024]; const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024];
const DeployEditor = () => { const DeployEditor = () => {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
const snap = useSnapshot(state); const snap = useSnapshot(state);
const router = useRouter(); const router = useRouter();
const { theme } = useTheme(); const { theme } = useTheme();
const [showContent, setShowContent] = useState(false); const [showContent, setShowContent] = useState(false);
const activeFile = snap.files[snap.active]; const activeFile = snap.files[snap.active]?.compiledContent
? snap.files[snap.active]
: snap.files.filter(file => file.compiledContent)[0];
const compiledSize = activeFile?.compiledContent?.byteLength || 0; const compiledSize = activeFile?.compiledContent?.byteLength || 0;
const color = const color =
compiledSize > FILESIZE_BREAKPOINTS[1] compiledSize > FILESIZE_BREAKPOINTS[1]
@@ -43,6 +36,10 @@ const DeployEditor = () => {
? "$warning" ? "$warning"
: "$success"; : "$success";
const isContentChanged =
activeFile && activeFile.compiledValueSnapshot !== activeFile.content;
// const hasDeployErros = activeFile && activeFile.containsErrors;
const CompiledStatView = activeFile && ( const CompiledStatView = activeFile && (
<Flex <Flex
column column
@@ -60,15 +57,30 @@ const DeployEditor = () => {
{activeFile?.lastCompiled && ( {activeFile?.lastCompiled && (
<ReactTimeAgo date={activeFile.lastCompiled} locale="en-US" /> <ReactTimeAgo date={activeFile.lastCompiled} locale="en-US" />
)} )}
{activeFile.compiledContent?.byteLength && ( {activeFile.compiledContent?.byteLength && (
<Text css={{ ml: "$2", color }}> <Text css={{ ml: "$2", color }}>
({filesize(activeFile.compiledContent.byteLength)}) ({filesize(activeFile.compiledContent.byteLength)})
</Text> </Text>
)} )}
</Flex> </Flex>
{activeFile.compiledContent?.byteLength &&
activeFile.compiledContent?.byteLength >= 64000 && (
<Flex css={{ flexDirection: "column", py: "$3", pb: "$1" }}>
<Text css={{ ml: "$2", color: "$error" }}>
File size is larger than 64kB, cannot set hook!
</Text>
</Flex>
)}
<Button variant="link" onClick={() => setShowContent(true)}> <Button variant="link" onClick={() => setShowContent(true)}>
View as WAT-file View as WAT-file
</Button> </Button>
{isContentChanged && (
<Text warning>
File contents were changed after last compile, compile again to
incorporate your latest changes in the build.
</Text>
)}
</Flex> </Flex>
); );
const NoContentView = !snap.loading && router.isReady && ( const NoContentView = !snap.loading && router.isReady && (
@@ -88,7 +100,7 @@ const DeployEditor = () => {
</Text> </Text>
); );
const isContent = const isContent =
snap.files?.filter((file) => file.compiledWatContent).length > 0 && snap.files?.filter(file => file.compiledWatContent).length > 0 &&
router.isReady; router.isReady;
return ( return (
<Box <Box
@@ -115,32 +127,38 @@ const DeployEditor = () => {
) : !showContent ? ( ) : !showContent ? (
CompiledStatView CompiledStatView
) : ( ) : (
<Editor <Monaco
className="hooks-editor" className="hooks-editor"
defaultLanguage={"wat"} defaultLanguage={"wat"}
language={"wat"} language={"wat"}
path={`file://tmp/c/${snap.files?.[snap.active]?.name}.wat`} path={`file://tmp/c/${activeFile?.name}.wat`}
value={snap.files?.[snap.active]?.compiledWatContent || ""} value={activeFile?.compiledWatContent || ""}
beforeMount={(monaco) => { beforeMount={monaco => {
monaco.languages.register({ id: "wat" }); monaco.languages.register({ id: "wat" });
monaco.languages.setLanguageConfiguration("wat", wat.config); monaco.languages.setLanguageConfiguration("wat", wat.config);
monaco.languages.setMonarchTokensProvider("wat", wat.tokens); monaco.languages.setMonarchTokensProvider("wat", wat.tokens);
if (!state.editorCtx) {
state.editorCtx = ref(monaco.editor);
// @ts-expect-error
monaco.editor.defineTheme("dark", dark);
// @ts-expect-error
monaco.editor.defineTheme("light", light);
}
}} }}
onMount={(editor, monaco) => { onMount={editor => {
editorRef.current = editor;
editor.updateOptions({ editor.updateOptions({
glyphMargin: true, glyphMargin: true,
readOnly: true, readOnly: true,
}); });
}} }}
theme={theme === "dark" ? "dark" : "light"} theme={theme === "dark" ? "dark" : "light"}
overlay={
<Flex
css={{
m: "$1",
ml: "auto",
fontSize: "$sm",
color: "$textMuted",
}}
>
<Link onClick={() => setShowContent(false)}>
Exit editor mode
</Link>
</Flex>
}
/> />
)} )}
</Container> </Container>

View File

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

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useCallback, useRef } from "react";
import { import {
Plus, Plus,
Share, Share,
@@ -101,7 +101,7 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
if (!filename) { if (!filename) {
return { error: "You need to add filename" }; return { error: "You need to add filename" };
} }
if (snap.files.find(file => file.name === filename)) { if (snap.files.find((file) => file.name === filename)) {
return { error: "Filename already exists." }; return { error: "Filename already exists." };
} }
@@ -132,22 +132,55 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
createNewFile(filename); createNewFile(filename);
setFilename(""); setFilename("");
}, [filename, setIsNewfileDialogOpen, setFilename, validateFilename]); }, [filename, setIsNewfileDialogOpen, setFilename, validateFilename]);
const scrollRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const files = snap.files; const files = snap.files;
return ( return (
<Flex css={{ flexShrink: 0, gap: "$0" }}> <Flex css={{ flexShrink: 0, gap: "$0" }}>
<Flex <Flex
id="kissa"
ref={scrollRef}
css={{ css={{
overflowX: "scroll", overflowX: "scroll",
overflowY: "hidden",
py: "$3", py: "$3",
pb: "$0",
flex: 1, flex: 1,
"&::-webkit-scrollbar": { "&::-webkit-scrollbar": {
height: 0, height: "0.3em",
background: "transparent", background: "rgba(0,0,0,.0)",
},
"&::-webkit-scrollbar-gutter": "stable",
"&::-webkit-scrollbar-thumb": {
backgroundColor: "rgba(0,0,0,.2)",
outline: "0px",
borderRadius: "9999px",
},
scrollbarColor: "rgba(0,0,0,.2) rgba(0,0,0,0)",
scrollbarGutter: "stable",
scrollbarWidth: "thin",
".dark &": {
"&::-webkit-scrollbar": {
background: "rgba(0,0,0,.0)",
},
"&::-webkit-scrollbar-gutter": "stable",
"&::-webkit-scrollbar-thumb": {
backgroundColor: "rgba(255,255,255,.2)",
outline: "0px",
borderRadius: "9999px",
},
scrollbarColor: "rgba(255,255,255,.2) rgba(0,0,0,0)",
scrollbarGutter: "stable",
scrollbarWidth: "thin",
}, },
}} }}
onWheelCapture={(e) => {
if (scrollRef.current) {
scrollRef.current.scrollLeft += e.deltaY;
}
}}
> >
<Container css={{ flex: 1 }}> <Container css={{ flex: 1 }} ref={containerRef}>
<Stack <Stack
css={{ css={{
gap: "$3", gap: "$3",
@@ -233,8 +266,8 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
<Label>Filename</Label> <Label>Filename</Label>
<Input <Input
value={filename} value={filename}
onChange={e => setFilename(e.target.value)} onChange={(e) => setFilename(e.target.value)}
onKeyPress={e => { onKeyPress={(e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
handleConfirm(); handleConfirm();
} }
@@ -509,8 +542,8 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
type="number" type="number"
min="1" min="1"
value={editorSettings.tabSize} value={editorSettings.tabSize}
onChange={e => onChange={(e) =>
setEditorSettings(curr => ({ setEditorSettings((curr) => ({
...curr, ...curr,
tabSize: Number(e.target.value), tabSize: Number(e.target.value),
})) }))

View File

@@ -1,6 +1,5 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import { useSnapshot, ref } from "valtio"; import { useSnapshot, ref } from "valtio";
import Editor, { loader } from "@monaco-editor/react";
import type monaco from "monaco-editor"; import type monaco from "monaco-editor";
import { ArrowBendLeftUp } from "phosphor-react"; import { ArrowBendLeftUp } from "phosphor-react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
@@ -8,8 +7,6 @@ import { useRouter } from "next/router";
import Box from "./Box"; import Box from "./Box";
import Container from "./Container"; import Container from "./Container";
import dark from "../theme/editor/amy.json";
import light from "../theme/editor/xcode_default.json";
import { saveFile } from "../state/actions"; import { saveFile } from "../state/actions";
import { apiHeaderFiles } from "../state/constants"; import { apiHeaderFiles } from "../state/constants";
import state from "../state"; import state from "../state";
@@ -22,16 +19,12 @@ import { listen } from "@codingame/monaco-jsonrpc";
import ReconnectingWebSocket from "reconnecting-websocket"; import ReconnectingWebSocket from "reconnecting-websocket";
import docs from "../xrpl-hooks-docs/docs"; import docs from "../xrpl-hooks-docs/docs";
import Monaco from "./Monaco";
loader.config({ import { saveAllFiles } from '../state/actions/saveFile';
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => { const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
const currPath = editor.getModel()?.uri.path; const currPath = editor.getModel()?.uri.path;
if (apiHeaderFiles.find((h) => currPath?.endsWith(h))) { if (apiHeaderFiles.find(h => currPath?.endsWith(h))) {
editor.updateOptions({ readOnly: true }); editor.updateOptions({ readOnly: true });
} else { } else {
editor.updateOptions({ readOnly: false }); editor.updateOptions({ readOnly: false });
@@ -48,7 +41,7 @@ const setMarkers = (monacoE: typeof monaco) => {
.getModelMarkers({}) .getModelMarkers({})
// Filter out the markers that are hooks specific // Filter out the markers that are hooks specific
.filter( .filter(
(marker) => marker =>
typeof marker?.code === "string" && typeof marker?.code === "string" &&
// Take only markers that starts with "hooks-" // Take only markers that starts with "hooks-"
marker?.code?.includes("hooks-") marker?.code?.includes("hooks-")
@@ -62,16 +55,16 @@ const setMarkers = (monacoE: typeof monaco) => {
// Add decoration (aka extra hoverMessages) to markers in the // Add decoration (aka extra hoverMessages) to markers in the
// exact same range (location) where the markers are // exact same range (location) where the markers are
const models = monacoE.editor.getModels(); const models = monacoE.editor.getModels();
models.forEach((model) => { models.forEach(model => {
decorations[model.id] = model?.deltaDecorations( decorations[model.id] = model?.deltaDecorations(
decorations?.[model.id] || [], decorations?.[model.id] || [],
markers markers
.filter((marker) => .filter(marker =>
marker?.resource.path marker?.resource.path
.split("/") .split("/")
.includes(`${state.files?.[state.active]?.name}`) .includes(`${state.files?.[state.active]?.name}`)
) )
.map((marker) => ({ .map(marker => ({
range: new monacoE.Range( range: new monacoE.Range(
marker.startLineNumber, marker.startLineNumber,
marker.startColumn, marker.startColumn,
@@ -119,6 +112,13 @@ const HooksEditor = () => {
setMarkers(monacoRef.current); setMarkers(monacoRef.current);
} }
}, [snap.active]); }, [snap.active]);
useEffect(() => {
return () => {
saveAllFiles();
};
}, []);
const file = snap.files[snap.active];
return ( return (
<Box <Box
css={{ css={{
@@ -133,16 +133,16 @@ const HooksEditor = () => {
> >
<EditorNavigation /> <EditorNavigation />
{snap.files.length > 0 && router.isReady ? ( {snap.files.length > 0 && router.isReady ? (
<Editor <Monaco
className="hooks-editor"
keepCurrentModel keepCurrentModel
defaultLanguage={snap.files?.[snap.active]?.language} defaultLanguage={file?.language}
language={snap.files?.[snap.active]?.language} language={file?.language}
path={`file:///work/c/${snap.files?.[snap.active]?.name}`} path={`file:///work/c/${file?.name}`}
defaultValue={snap.files?.[snap.active]?.content} defaultValue={file?.content}
beforeMount={(monaco) => { // onChange={val => (state.files[snap.active].content = val)} // Auto save?
beforeMount={monaco => {
if (!snap.editorCtx) { if (!snap.editorCtx) {
snap.files.forEach((file) => snap.files.forEach(file =>
monaco.editor.createModel( monaco.editor.createModel(
file.content, file.content,
file.language, file.language,
@@ -167,13 +167,13 @@ const HooksEditor = () => {
// listen when the web socket is opened // listen when the web socket is opened
listen({ listen({
webSocket: webSocket as WebSocket, webSocket: webSocket as WebSocket,
onConnection: (connection) => { onConnection: connection => {
// create and start the language client // create and start the language client
const languageClient = createLanguageClient(connection); const languageClient = createLanguageClient(connection);
const disposable = languageClient.start(); const disposable = languageClient.start();
connection.onClose(() => { connection.onClose(() => {
try { try {
// disposable.stop();
disposable.dispose(); disposable.dispose();
} catch (err) { } catch (err) {
console.log("err", err); console.log("err", err);
@@ -183,7 +183,6 @@ const HooksEditor = () => {
}); });
} }
// // hook editor to global state
// editor.updateOptions({ // editor.updateOptions({
// minimap: { // minimap: {
// enabled: false, // enabled: false,
@@ -192,10 +191,6 @@ const HooksEditor = () => {
// }); // });
if (!state.editorCtx) { if (!state.editorCtx) {
state.editorCtx = ref(monaco.editor); state.editorCtx = ref(monaco.editor);
// @ts-expect-error
monaco.editor.defineTheme("dark", dark);
// @ts-expect-error
monaco.editor.defineTheme("light", light);
} }
}} }}
onMount={(editor, monaco) => { onMount={(editor, monaco) => {
@@ -223,13 +218,13 @@ const HooksEditor = () => {
}); });
// Hacky way to hide Peek menu // Hacky way to hide Peek menu
editor.onContextMenu((e) => { editor.onContextMenu(e => {
const host = const host =
document.querySelector<HTMLElement>(".shadow-root-host"); document.querySelector<HTMLElement>(".shadow-root-host");
const contextMenuItems = const contextMenuItems =
host?.shadowRoot?.querySelectorAll("li.action-item"); host?.shadowRoot?.querySelectorAll("li.action-item");
contextMenuItems?.forEach((k) => { contextMenuItems?.forEach(k => {
// If menu item contains "Peek" lets hide it // If menu item contains "Peek" lets hide it
if (k.querySelector(".action-label")?.textContent === "Peek") { if (k.querySelector(".action-label")?.textContent === "Peek") {
// @ts-expect-error // @ts-expect-error

View File

@@ -6,7 +6,7 @@ import {
useState, useState,
useCallback, useCallback,
} from "react"; } from "react";
import { Notepad, Prohibit } from "phosphor-react"; import { IconProps, Notepad, Prohibit } from "phosphor-react";
import useStayScrolled from "react-stay-scrolled"; import useStayScrolled from "react-stay-scrolled";
import NextLink from "next/link"; import NextLink from "next/link";
@@ -24,6 +24,7 @@ interface ILogBox {
logs: ILog[]; logs: ILog[];
renderNav?: () => ReactNode; renderNav?: () => ReactNode;
enhanced?: boolean; enhanced?: boolean;
Icon?: FC<IconProps>;
} }
const LogBox: FC<ILogBox> = ({ const LogBox: FC<ILogBox> = ({
@@ -33,6 +34,7 @@ const LogBox: FC<ILogBox> = ({
children, children,
renderNav, renderNav,
enhanced, enhanced,
Icon = Notepad,
}) => { }) => {
const logRef = useRef<HTMLPreElement>(null); const logRef = useRef<HTMLPreElement>(null);
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef); const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
@@ -82,14 +84,14 @@ const LogBox: FC<ILogBox> = ({
gap: "$3", gap: "$3",
}} }}
> >
<Notepad size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text> <Icon size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
</Heading> </Heading>
<Flex <Flex
row row
align="center" align="center"
css={{ // css={{
width: "50%", // TODO make it max without breaking layout! // maxWidth: "100%", // TODO make it max without breaking layout!
}} // }}
> >
{renderNav?.()} {renderNav?.()}
</Flex> </Flex>
@@ -162,11 +164,11 @@ export const Log: FC<ILog> = ({
(str?: string): ReactNode => { (str?: string): ReactNode => {
if (!str || !accounts.length) return null; if (!str || !accounts.length) return null;
const pattern = `(${accounts.map((acc) => acc.address).join("|")})`; const pattern = `(${accounts.map(acc => acc.address).join("|")})`;
const res = regexifyString({ const res = regexifyString({
pattern: new RegExp(pattern, "gim"), pattern: new RegExp(pattern, "gim"),
decorator: (match, idx) => { decorator: (match, idx) => {
const name = accounts.find((acc) => acc.address === match)?.name; const name = accounts.find(acc => acc.address === match)?.name;
return ( return (
<Link <Link
key={match + idx} key={match + idx}
@@ -188,13 +190,13 @@ export const Log: FC<ILog> = ({
); );
let message: ReactNode; let message: ReactNode;
if (typeof _message === 'string') { if (typeof _message === "string") {
_message = _message.trim().replace(/\n /gi, "\n"); _message = _message.trim().replace(/\n /gi, "\n");
message = enrichAccounts(_message) if (_message) message = enrichAccounts(_message);
} else message = <Text muted>{'""'}</Text>
else { } else {
message = _message message = _message;
} }
const jsonData = enrichAccounts(_jsonData); const jsonData = enrichAccounts(_jsonData);

75
components/Monaco.tsx Normal file
View 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;

View File

@@ -30,12 +30,6 @@ import PanelBox from "./PanelBox";
import { templateFileIds } from "../state/constants"; import { templateFileIds } from "../state/constants";
import { styled } from "../stitches.config"; import { styled } from "../stitches.config";
import Starter from "../components/icons/Starter";
import Firewall from "../components/icons/Firewall";
import Notary from "../components/icons/Notary";
import Carbon from "../components/icons/Carbon";
import Peggy from "../components/icons/Peggy";
const ImageWrapper = styled(Flex, { const ImageWrapper = styled(Flex, {
position: "relative", position: "relative",
mt: "$2", mt: "$2",
@@ -301,66 +295,18 @@ const Navigation = () => {
}, },
}} }}
> >
<PanelBox {Object.values(templateFileIds).map((template) => (
as="a" <PanelBox
href={`/develop/${templateFileIds.starter}`} key={template.id}
> as="a"
<ImageWrapper> href={`/develop/${template.id}`}
<Starter /> >
</ImageWrapper> <ImageWrapper>{template.icon()}</ImageWrapper>
<Heading>Starter</Heading> <Heading>{template.name}</Heading>
<Text> <Text>{template.description}</Text>
Just a basic starter with essential imports, just </PanelBox>
accepts any transaction coming through ))}
</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.firewall}`}
css={{ alignItems: "flex-start" }}
>
<ImageWrapper>
<Firewall />
</ImageWrapper>
<Heading>Firewall</Heading>
<Text>
This Hook essentially checks a blacklist of accounts
</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.notary}`}
>
<ImageWrapper>
<Notary />
</ImageWrapper>
<Heading>Notary</Heading>
<Text>
Collecting signatures for multi-sign transactions
</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.carbon}`}
>
<ImageWrapper>
<Carbon />
</ImageWrapper>
<Heading>Carbon</Heading>
<Text>Send a percentage of sum to an address</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.peggy}`}
>
<ImageWrapper>
<Peggy />
</ImageWrapper>
<Heading>Peggy</Heading>
<Text>An oracle based stable coin hook</Text>
</PanelBox>
</Flex> </Flex>
</Flex> </Flex>
<DialogClose asChild> <DialogClose asChild>
@@ -394,6 +340,8 @@ const Navigation = () => {
height: 0, height: 0,
background: "transparent", background: "transparent",
}, },
scrollbarColor: "transparent",
scrollbarWidth: "none",
}} }}
> >
<Stack <Stack

View File

@@ -0,0 +1,338 @@
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 = [
...snap.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;

View File

@@ -22,12 +22,12 @@ import {
import { TTS, tts } from "../utils/hookOnCalculator"; import { TTS, tts } from "../utils/hookOnCalculator";
import { deployHook } from "../state/actions"; import { deployHook } from "../state/actions";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import state from "../state"; import state, { SelectOption } from "../state";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { prepareDeployHookTx, sha256 } from "../state/actions/deployHook"; import { prepareDeployHookTx, sha256 } from "../state/actions/deployHook";
import estimateFee from "../utils/estimateFee"; import estimateFee from "../utils/estimateFee";
const transactionOptions = Object.keys(tts).map((key) => ({ const transactionOptions = Object.keys(tts).map(key => ({
label: key, label: key,
value: key as keyof TTS, value: key as keyof TTS,
})); }));
@@ -56,9 +56,22 @@ export type SetHookData = {
export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo( export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
({ accountAddress }) => { ({ accountAddress }) => {
const snap = useSnapshot(state); const snap = useSnapshot(state);
const account = snap.accounts.find((acc) => acc.address === accountAddress); const activeFile = snap.files[snap.active]?.compiledContent
? snap.files[snap.active]
: snap.files.filter(file => file.compiledContent)[0];
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false); const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
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 { const {
register, register,
handleSubmit, handleSubmit,
@@ -68,11 +81,13 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
getValues, getValues,
formState: { errors }, formState: { errors },
} = useForm<SetHookData>({ } = useForm<SetHookData>({
defaultValues: { defaultValues: snap.deployValues?.[activeFile?.name]
HookNamespace: ? snap.deployValues[activeFile?.name]
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "", : {
Invoke: transactionOptions.filter((to) => to.label === "ttPAYMENT"), HookNamespace:
}, snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "",
Invoke: transactionOptions.filter(to => to.label === "ttPAYMENT"),
},
}); });
const { fields, append, remove } = useFieldArray({ const { fields, append, remove } = useFieldArray({
control, control,
@@ -80,15 +95,30 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
}); });
const [formInitialized, setFormInitialized] = useState(false); const [formInitialized, setFormInitialized] = useState(false);
const [estimateLoading, setEstimateLoading] = useState(false); const [estimateLoading, setEstimateLoading] = useState(false);
const watchedFee = watch("Fee");
// Update value if activeWat changes // Update value if activeWat changes
useEffect(() => { useEffect(() => {
setValue( const defaultValue = snap.deployValues?.[activeFile?.name]
"HookNamespace", ? snap.deployValues?.[activeFile?.name].HookNamespace
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "" : snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "";
); setValue("HookNamespace", defaultValue);
setFormInitialized(true); setFormInitialized(true);
}, [snap.activeWat, snap.files, setValue]); }, [
snap.activeWat,
snap.files,
setValue,
activeFile?.name,
snap.deployValues,
]);
useEffect(() => {
if (
watchedFee &&
(watchedFee.includes(".") || watchedFee.includes(","))
) {
setValue("Fee", watchedFee.replaceAll(".", "").replaceAll(",", ""));
}
}, [watchedFee, setValue]);
// const { // const {
// fields: grantFields, // fields: grantFields,
// append: grantAppend, // append: grantAppend,
@@ -100,7 +130,9 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
const [hashedNamespace, setHashedNamespace] = useState(""); const [hashedNamespace, setHashedNamespace] = useState("");
const namespace = watch( const namespace = watch(
"HookNamespace", "HookNamespace",
snap.files?.[snap.active]?.name?.split(".")?.[0] || "" snap.deployValues?.[activeFile?.name]
? snap.deployValues?.[activeFile?.name].HookNamespace
: snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
); );
const calculateHashedValue = useCallback(async () => { const calculateHashedValue = useCallback(async () => {
const hashedVal = await sha256(namespace); const hashedVal = await sha256(namespace);
@@ -121,21 +153,28 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
} }
const res = await estimateFee(tx, account); const res = await estimateFee(tx, account);
if (res && res.base_fee) { if (res && res.base_fee) {
setValue("Fee", res.base_fee); setValue("Fee", Math.round(Number(res.base_fee || "")).toString());
} }
})(); })();
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [formInitialized]); }, [formInitialized]);
if (!account) { const tooLargeFile = () => {
return null; const activeFile = snap.files[snap.active].compiledContent
} ? snap.files[snap.active]
: snap.files.filter(file => file.compiledContent)[0];
return Boolean(
activeFile?.compiledContent?.byteLength &&
activeFile?.compiledContent?.byteLength >= 64000
);
};
const onSubmit: SubmitHandler<SetHookData> = async (data) => { const onSubmit: SubmitHandler<SetHookData> = async (data) => {
const currAccount = state.accounts.find( const currAccount = state.accounts.find(
(acc) => acc.address === account.address (acc) => acc.address === account?.address
); );
if (!account) return;
if (currAccount) currAccount.isLoading = true; if (currAccount) currAccount.isLoading = true;
const res = await deployHook(account, data); const res = await deployHook(account, data);
if (currAccount) currAccount.isLoading = false; if (currAccount) currAccount.isLoading = false;
@@ -155,8 +194,10 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
uppercase uppercase
variant={"secondary"} variant={"secondary"}
disabled={ disabled={
!account ||
account.isLoading || account.isLoading ||
!snap.files.filter((file) => file.compiledWatContent).length !snap.files.filter(file => file.compiledWatContent).length ||
tooLargeFile()
} }
> >
Set Hook Set Hook
@@ -167,14 +208,22 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
<DialogTitle>Deploy configuration</DialogTitle> <DialogTitle>Deploy configuration</DialogTitle>
<DialogDescription as="div"> <DialogDescription as="div">
<Stack css={{ width: "100%", flex: 1 }}> <Stack css={{ width: "100%", flex: 1 }}>
<Box css={{ width: "100%" }}>
<Label>Account</Label>
<Select
instanceId="deploy-account"
placeholder="Select account"
hideSelectedOptions
options={accountOptions}
value={selectedAccount}
onChange={(acc: any) => setSelectedAccount(acc)}
/>
</Box>
<Box css={{ width: "100%" }}> <Box css={{ width: "100%" }}>
<Label>Invoke on transactions</Label> <Label>Invoke on transactions</Label>
<Controller <Controller
name="Invoke" name="Invoke"
control={control} control={control}
defaultValue={transactionOptions.filter(
(to) => to.label === "ttPAYMENT"
)}
render={({ field }) => ( render={({ field }) => (
<Select <Select
{...field} {...field}
@@ -191,9 +240,6 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
<Input <Input
{...register("HookNamespace", { required: true })} {...register("HookNamespace", { required: true })}
autoComplete={"off"} autoComplete={"off"}
defaultValue={
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
}
/> />
{errors.HookNamespace?.type === "required" && ( {errors.HookNamespace?.type === "required" && (
<Box css={{ display: "inline", color: "$red11" }}> <Box css={{ display: "inline", color: "$red11" }}>
@@ -256,6 +302,12 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
type="number" type="number"
{...register("Fee", { required: true })} {...register("Fee", { required: true })}
autoComplete={"off"} autoComplete={"off"}
onKeyPress={e => {
if (e.key === "." || e.key === ",") {
e.preventDefault();
}
}}
step="1"
defaultValue={10000} defaultValue={10000}
css={{ css={{
"-moz-appearance": "textfield", "-moz-appearance": "textfield",
@@ -282,8 +334,9 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
alignContent: "center", alignContent: "center",
display: "flex", display: "flex",
}} }}
onClick={async (e) => { onClick={async e => {
e.preventDefault(); e.preventDefault();
if (!account) return;
setEstimateLoading(true); setEstimateLoading(true);
const formValues = getValues(); const formValues = getValues();
try { try {
@@ -295,7 +348,12 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
const res = await estimateFee(tx, account); const res = await estimateFee(tx, account);
if (res && res.base_fee) { if (res && res.base_fee) {
setValue("Fee", res.base_fee); setValue(
"Fee",
Math.round(
Number(res.base_fee || "")
).toString()
);
} }
} }
} catch (err) {} } catch (err) {}
@@ -377,7 +435,7 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
<Button <Button
variant="primary" variant="primary"
type="submit" type="submit"
isLoading={account.isLoading} isLoading={account?.isLoading}
> >
Set Hook Set Hook
</Button> </Button>

View File

@@ -31,13 +31,15 @@ interface TabProps {
// TODO customise messages shown // TODO customise messages shown
interface Props { interface Props {
label?: string;
activeIndex?: number; activeIndex?: number;
activeHeader?: string; activeHeader?: string;
headless?: boolean; headless?: boolean;
children: ReactElement<TabProps>[]; children: ReactElement<TabProps>[];
keepAllAlive?: boolean; keepAllAlive?: boolean;
defaultExtension?: string; defaultExtension?: string;
forceDefaultExtension?: boolean; appendDefaultExtension?: boolean;
allowedExtensions?: string[];
onCreateNewTab?: (name: string) => any; onCreateNewTab?: (name: string) => any;
onCloseTab?: (index: number, header?: string) => any; onCloseTab?: (index: number, header?: string) => any;
onChangeActive?: (index: number, header?: string) => any; onChangeActive?: (index: number, header?: string) => any;
@@ -46,6 +48,7 @@ interface Props {
export const Tab = (props: TabProps) => null; export const Tab = (props: TabProps) => null;
export const Tabs = ({ export const Tabs = ({
label = "Tab",
children, children,
activeIndex, activeIndex,
activeHeader, activeHeader,
@@ -55,7 +58,8 @@ export const Tabs = ({
onCloseTab, onCloseTab,
onChangeActive, onChangeActive,
defaultExtension = "", defaultExtension = "",
forceDefaultExtension, appendDefaultExtension = false,
allowedExtensions,
}: Props) => { }: Props) => {
const [active, setActive] = useState(activeIndex || 0); const [active, setActive] = useState(activeIndex || 0);
const tabs: TabProps[] = children.map(elem => elem.props); const tabs: TabProps[] = children.map(elem => elem.props);
@@ -86,9 +90,13 @@ export const Tabs = ({
if (tabs.find(tab => tab.header === tabname)) { if (tabs.find(tab => tab.header === tabname)) {
return { error: "Name already exists." }; return { error: "Name already exists." };
} }
const ext = tabname.split(".").pop() || "";
if (allowedExtensions && !allowedExtensions.includes(ext)) {
return { error: "This file extension is not allowed!" };
}
return { error: null }; return { error: null };
}, },
[tabs] [allowedExtensions, tabs]
); );
const handleActiveChange = useCallback( const handleActiveChange = useCallback(
@@ -101,9 +109,11 @@ export const Tabs = ({
const handleCreateTab = useCallback(() => { const handleCreateTab = useCallback(() => {
// add default extension in case omitted // add default extension in case omitted
let _tabname = tabname.includes(".") ? tabname : tabname + defaultExtension; let _tabname = tabname.includes(".")
if (forceDefaultExtension && !_tabname.endsWith(defaultExtension)) { ? tabname
_tabname = _tabname + defaultExtension; : `${tabname}.${defaultExtension}`;
if (appendDefaultExtension && !_tabname.endsWith(defaultExtension)) {
_tabname = `${_tabname}.${defaultExtension}`;
} }
const chk = validateTabname(_tabname); const chk = validateTabname(_tabname);
@@ -122,7 +132,7 @@ export const Tabs = ({
}, [ }, [
tabname, tabname,
defaultExtension, defaultExtension,
forceDefaultExtension, appendDefaultExtension,
validateTabname, validateTabname,
onCreateNewTab, onCreateNewTab,
handleActiveChange, handleActiveChange,
@@ -206,13 +216,13 @@ export const Tabs = ({
size="sm" size="sm"
css={{ alignItems: "center", px: "$2", mr: "$3" }} 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> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogTitle>Create new tab</DialogTitle> <DialogTitle>Create new {label.toLocaleLowerCase()}</DialogTitle>
<DialogDescription> <DialogDescription>
<Label>Tabname</Label> <Label>{label} name</Label>
<Input <Input
value={tabname} value={tabname}
onChange={e => setTabname(e.target.value)} onChange={e => setTabname(e.target.value)}

View File

@@ -7,20 +7,35 @@ const Text = styled("span", {
variants: { variants: {
small: { small: {
true: { true: {
fontSize: '$xs' fontSize: "$xs",
} },
}, },
muted: { muted: {
true: { true: {
color: '$mauve9' color: "$mauve9",
} },
},
error: {
true: {
color: "$error",
},
},
warning: {
true: {
color: "$warning",
},
}, },
monospace: { monospace: {
true: { true: {
fontFamily: '$monospace' fontFamily: "$monospace",
} },
} },
} block: {
true: {
display: "block",
},
},
},
}); });
export default Text; export default Text;

115
components/Textarea.tsx Normal file
View 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;

View File

@@ -14,6 +14,8 @@ import Button from "../Button";
import Flex from "../Flex"; import Flex from "../Flex";
import { TxJson } from "./json"; import { TxJson } from "./json";
import { TxUI } from "./ui"; import { TxUI } from "./ui";
import { default as _estimateFee } from "../../utils/estimateFee";
import toast from 'react-hot-toast';
export interface TransactionProps { export interface TransactionProps {
header: string; header: string;
@@ -76,13 +78,19 @@ const Transaction: FC<TransactionProps> = ({
} else { } else {
setState({ txIsDisabled: false }); setState({ txIsDisabled: false });
} }
}, [selectedAccount?.value, selectedTransaction?.value, setState, txIsLoading]); }, [
selectedAccount?.value,
selectedTransaction?.value,
setState,
txIsLoading,
]);
const submitTest = useCallback(async () => { const submitTest = useCallback(async () => {
let st: TransactionState | undefined; let st: TransactionState | undefined;
const tt = txState.selectedTransaction?.value;
if (viewType === "json") { if (viewType === "json") {
// save the editor state first // save the editor state first
const pst = prepareState(editorValue || '', txState); const pst = prepareState(editorValue || "", tt);
if (!pst) return; if (!pst) return;
st = setState(pst); st = setState(pst);
@@ -102,7 +110,7 @@ const Transaction: FC<TransactionProps> = ({
const options = prepareOptions(st); const options = prepareOptions(st);
if (options.Destination === null) { if (options.Destination === null) {
throw Error("Destination account cannot be null") throw Error("Destination account cannot be null");
} }
await sendTransaction(account, options, { logPrefix }); await sendTransaction(account, options, { logPrefix });
@@ -116,7 +124,17 @@ const Transaction: FC<TransactionProps> = ({
} }
} }
setState({ txIsLoading: false }); setState({ txIsLoading: false });
}, [viewType, accounts, txIsDisabled, setState, header, editorValue, txState, selectedAccount?.value, prepareOptions]); }, [
viewType,
accounts,
txIsDisabled,
setState,
header,
editorValue,
txState,
selectedAccount?.value,
prepareOptions,
]);
const resetState = useCallback(() => { const resetState = useCallback(() => {
modifyTransaction(header, { viewType }, { replaceState: true }); modifyTransaction(header, { viewType }, { replaceState: true });
@@ -129,6 +147,31 @@ const Transaction: FC<TransactionProps> = ({
[editorSavedValue, editorSettings.tabSize, prepareOptions] [editorSavedValue, editorSettings.tabSize, prepareOptions]
); );
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 ( return (
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}> <Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
{viewType === "json" ? ( {viewType === "json" ? (
@@ -137,9 +180,10 @@ const Transaction: FC<TransactionProps> = ({
header={header} header={header}
state={txState} state={txState}
setState={setState} setState={setState}
estimateFee={estimateFee}
/> />
) : ( ) : (
<TxUI state={txState} setState={setState} /> <TxUI state={txState} setState={setState} estimateFee={estimateFee} />
)} )}
<Flex <Flex
row row

View File

@@ -1,9 +1,4 @@
import Editor, { loader, useMonaco } from "@monaco-editor/react";
import { FC, useCallback, useEffect, useState } from "react"; import { FC, useCallback, useEffect, useState } from "react";
import { useTheme } from "next-themes";
import dark from "../../theme/editor/amy.json";
import light from "../../theme/editor/xcode_default.json";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import state, { import state, {
prepareState, prepareState,
@@ -11,24 +6,20 @@ import state, {
TransactionState, TransactionState,
} from "../../state"; } from "../../state";
import Text from "../Text"; import Text from "../Text";
import Flex from "../Flex"; import { Flex, Link } from "..";
import { Link } from "..";
import { showAlert } from "../../state/actions/showAlert"; import { showAlert } from "../../state/actions/showAlert";
import { parseJSON } from "../../utils/json"; import { parseJSON } from "../../utils/json";
import { extractSchemaProps } from "../../utils/schema"; import { extractSchemaProps } from "../../utils/schema";
import amountSchema from "../../content/amount-schema.json"; import amountSchema from "../../content/amount-schema.json";
import Monaco from "../Monaco";
loader.config({ import type monaco from "monaco-editor";
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
interface JsonProps { interface JsonProps {
value?: string; value?: string;
header?: string; header?: string;
setState: (pTx?: Partial<TransactionState> | undefined) => void; setState: (pTx?: Partial<TransactionState> | undefined) => void;
state: TransactionState; state: TransactionState;
estimateFee?: () => Promise<string | undefined>;
} }
export const TxJson: FC<JsonProps> = ({ export const TxJson: FC<JsonProps> = ({
@@ -38,22 +29,36 @@ export const TxJson: FC<JsonProps> = ({
setState, setState,
}) => { }) => {
const { editorSettings, accounts } = useSnapshot(state); const { editorSettings, accounts } = useSnapshot(state);
const { editorValue = value, selectedTransaction } = txState; const { editorValue = value, estimatedFee } = txState;
const { theme } = useTheme();
const [hasUnsaved, setHasUnsaved] = useState(false); const [hasUnsaved, setHasUnsaved] = useState(false);
const [currTxType, setCurrTxType] = useState<string | undefined>(
txState.selectedTransaction?.value
);
useEffect(() => { useEffect(() => {
setState({ editorValue: value }); setState({ editorValue: value });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]); }, [value]);
useEffect(() => {
const parsed = parseJSON(editorValue);
if (!parsed) return;
const tt = parsed.TransactionType;
const tx = transactionsData.find(t => t.TransactionType === tt);
if (tx) setCurrTxType(tx.TransactionType);
else {
setCurrTxType(undefined);
}
}, [editorValue]);
useEffect(() => { useEffect(() => {
if (editorValue === value) setHasUnsaved(false); if (editorValue === value) setHasUnsaved(false);
else setHasUnsaved(true); else setHasUnsaved(true);
}, [editorValue, value]); }, [editorValue, value]);
const saveState = (value: string, txState: TransactionState) => { const saveState = (value: string, transactionType?: string) => {
const tx = prepareState(value, txState); const tx = prepareState(value, transactionType);
if (tx) setState(tx); if (tx) setState(tx);
}; };
@@ -68,7 +73,7 @@ export const TxJson: FC<JsonProps> = ({
const onExit = (value: string) => { const onExit = (value: string) => {
const options = parseJSON(value); const options = parseJSON(value);
if (options) { if (options) {
saveState(value, txState); saveState(value, currTxType);
return; return;
} }
showAlert("Error!", { showAlert("Error!", {
@@ -79,12 +84,10 @@ export const TxJson: FC<JsonProps> = ({
}); });
}; };
const path = `file:///${header}`; const getSchemas = useCallback(async (): Promise<any[]> => {
const monaco = useMonaco(); const txObj = transactionsData.find(
td => td.TransactionType === currTxType
const getSchemas = useCallback((): any[] => { );
const tt = selectedTransaction?.value;
const txObj = transactionsData.find(td => td.TransactionType === tt);
let genericSchemaProps: any; let genericSchemaProps: any;
if (txObj) { if (txObj) {
@@ -98,7 +101,6 @@ export const TxJson: FC<JsonProps> = ({
{} {}
); );
} }
return [ return [
{ {
uri: "file:///main-schema.json", // id of the first schema uri: "file:///main-schema.json", // id of the first schema
@@ -130,6 +132,9 @@ export const TxJson: FC<JsonProps> = ({
Amount: { Amount: {
$ref: "file:///amount-schema.json", $ref: "file:///amount-schema.json",
}, },
Fee: {
$ref: "file:///fee-schema.json",
},
}, },
}, },
}, },
@@ -141,65 +146,80 @@ export const TxJson: FC<JsonProps> = ({
enum: accounts.map(acc => acc.address), 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, ...amountSchema,
}, },
]; ];
}, [accounts, header, selectedTransaction?.value]); }, [accounts, currTxType, estimatedFee, header]);
const [monacoInst, setMonacoInst] = useState<typeof monaco>();
useEffect(() => { useEffect(() => {
if (!monaco) return; if (!monacoInst) return;
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ getSchemas().then(schemas => {
validate: true, monacoInst.languages.json.jsonDefaults.setDiagnosticsOptions({
schemas: getSchemas(), validate: true,
schemas,
});
}); });
}, [getSchemas, monaco]); }, [getSchemas, monacoInst]);
return ( return (
<Flex <Monaco
fluid rootProps={{
column css: { height: "calc(100% - 45px)" },
css={{ height: "calc(100% - 45px)", position: "relative" }} }}
> language={"json"}
<Editor id={header}
className="hooks-editor" height="100%"
language={"json"} value={editorValue}
path={path} onChange={val => setState({ editorValue: val })}
height="100%" onMount={(editor, monaco) => {
beforeMount={monaco => { editor.updateOptions({
monaco.editor.defineTheme("dark", dark as any); minimap: { enabled: false },
monaco.editor.defineTheme("light", light as any); glyphMargin: true,
}} tabSize: editorSettings.tabSize,
value={editorValue} dragAndDrop: true,
onChange={val => setState({ editorValue: val })} fontSize: 14,
onMount={(editor, monaco) => { });
editor.updateOptions({
minimap: { enabled: false },
glyphMargin: true,
tabSize: editorSettings.tabSize,
dragAndDrop: true,
fontSize: 14,
});
// register onExit cb setMonacoInst(monaco);
const model = editor.getModel(); // register onExit cb
model?.onWillDispose(() => onExit(model.getValue())); const model = editor.getModel();
model?.onWillDispose(() => onExit(model.getValue()));
// set json defaults }}
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ overlay={
validate: true, hasUnsaved ? (
schemas: getSchemas(), <Flex
}); row
}} align="center"
theme={theme === "dark" ? "dark" : "light"} css={{ fontSize: "$xs", color: "$textMuted", ml: 'auto' }}
/> >
{hasUnsaved && ( <Text muted small>
<Text muted small css={{ position: "absolute", bottom: 0, right: 0 }}> This file has unsaved changes.
This file has unsaved changes.{" "} </Text>
<Link onClick={() => saveState(editorValue, txState)}>save</Link>{" "} <Link
<Link onClick={discardChanges}>discard</Link> css={{ ml: "$1" }}
</Text> onClick={() => saveState(editorValue, currTxType)}
)} >
</Flex> save
</Link>
<Link css={{ ml: "$1" }} onClick={discardChanges}>
discard
</Link>
</Flex>
) : undefined
}
/>
); );
}; };

View File

@@ -1,4 +1,4 @@
import { FC } from "react"; import { FC, useCallback, useEffect, useState } from "react";
import Container from "../Container"; import Container from "../Container";
import Flex from "../Flex"; import Flex from "../Flex";
import Input from "../Input"; import Input from "../Input";
@@ -9,17 +9,27 @@ import {
TransactionState, TransactionState,
transactionsData, transactionsData,
TxFields, TxFields,
getTxFields,
} from "../../state/transactions"; } from "../../state/transactions";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import state from "../../state"; import state from "../../state";
import { streamState } from "../DebugStream"; import { streamState } from "../DebugStream";
import { Button } from "..";
import Textarea from "../Textarea";
interface UIProps { interface UIProps {
setState: (pTx?: Partial<TransactionState> | undefined) => void; setState: (
pTx?: Partial<TransactionState> | undefined
) => TransactionState | undefined;
state: TransactionState; state: TransactionState;
estimateFee?: (...arg: any) => Promise<string | undefined>;
} }
export const TxUI: FC<UIProps> = ({ state: txState, setState }) => { export const TxUI: FC<UIProps> = ({
state: txState,
setState,
estimateFee,
}) => {
const { accounts } = useSnapshot(state); const { accounts } = useSnapshot(state);
const { const {
selectedAccount, selectedAccount,
@@ -28,6 +38,7 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
txFields, txFields,
} = txState; } = txState;
const transactionsOptions = transactionsData.map(tx => ({ const transactionsOptions = transactionsData.map(tx => ({
value: tx.TransactionType, value: tx.TransactionType,
label: tx.TransactionType, label: tx.TransactionType,
@@ -45,33 +56,58 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
})) }))
.filter(acc => acc.value !== selectedAccount?.value); .filter(acc => acc.value !== selectedAccount?.value);
const resetOptions = (tt: string) => { const [feeLoading, setFeeLoading] = useState(false);
const txFields: TxFields | undefined = transactionsData.find(
tx => tx.TransactionType === tt
);
if (!txFields) return setState({ txFields: {} }); const resetOptions = useCallback(
(tt: string) => {
const _txFields = Object.keys(txFields) const fields = getTxFields(tt);
.filter(key => !["TransactionType", "Account", "Sequence"].includes(key)) if (!fields.Destination) setState({ selectedDestAccount: null });
.reduce<TxFields>( return setState({ txFields: fields });
(tf, key) => ((tf[key as keyof TxFields] = (txFields as any)[key]), tf), },
{} [setState]
); );
if (!_txFields.Destination) setState({ selectedDestAccount: null });
setState({ txFields: _txFields });
};
const handleSetAccount = (acc: SelectOption) => { const handleSetAccount = (acc: SelectOption) => {
setState({ selectedAccount: acc }); setState({ selectedAccount: acc });
streamState.selectedAccount = acc; streamState.selectedAccount = acc;
}; };
const handleChangeTxType = (tt: SelectOption) => { const handleSetField = useCallback(
setState({ selectedTransaction: tt }); (field: keyof TxFields, value: string, opFields?: TxFields) => {
resetOptions(tt.value); const fields = opFields || txFields;
}; const obj = fields[field];
setState({
txFields: {
...fields,
[field]: typeof obj === "object" ? { ...obj, $value: value } : value,
},
});
},
[setState, txFields]
);
const 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 = resetOptions(tt.value);
handleEstimateFee(newState, true);
},
[handleEstimateFee, resetOptions, setState]
);
const specialFields = ["TransactionType", "Account", "Destination"]; const specialFields = ["TransactionType", "Account", "Destination"];
@@ -79,6 +115,21 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
k => !specialFields.includes(k) k => !specialFields.includes(k)
) as [keyof TxFields]; ) as [keyof TxFields];
const switchToJson = () =>
setState({ editorSavedValue: null, viewType: "json" });
// default tx
useEffect(() => {
if (selectedTransaction?.value) return;
const defaultOption = transactionsOptions.find(
tt => tt.value === "Payment"
);
if (defaultOption) {
handleChangeTxType(defaultOption);
}
}, [handleChangeTxType, selectedTransaction?.value, transactionsOptions]);
return ( return (
<Container <Container
css={{ css={{
@@ -87,7 +138,7 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
height: "calc(100% - 45px)", height: "calc(100% - 45px)",
}} }}
> >
<Flex column fluid css={{ height: "100%", overflowY: "auto" }}> <Flex column fluid css={{ height: "100%", overflowY: "auto", pr: "$1" }}>
<Flex <Flex
row row
fluid fluid
@@ -165,7 +216,7 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
let value: string | undefined; let value: string | undefined;
if (typeof _value === "object") { if (typeof _value === "object") {
if (_value.$type === "json" && typeof _value.$value === "object") { if (_value.$type === "json" && typeof _value.$value === "object") {
value = JSON.stringify(_value.$value); value = JSON.stringify(_value.$value, null, 2);
} else { } else {
value = _value.$value.toString(); value = _value.$value.toString();
} }
@@ -173,37 +224,99 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
value = _value?.toString(); value = _value?.toString();
} }
let isXrp = typeof _value === "object" && _value.$type === "xrp"; 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 ( return (
<Flex <Flex column key={field} css={{ mb: "$2", pr: "1px" }}>
key={field} <Flex
row row
fluid fluid
css={{ css={{
justifyContent: "flex-end", justifyContent: "flex-end",
alignItems: "center", alignItems: "center",
mb: "$3", position: "relative",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
{field + (isXrp ? " (XRP)" : "")}:{" "}
</Text>
<Input
value={value}
onChange={e => {
setState({
txFields: {
...txFields,
[field]:
typeof _value === "object"
? { ..._value, $value: e.target.value }
: e.target.value,
},
});
}} }}
css={{ width: "70%", flex: "inherit" }} >
/> <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>
); );
})} })}

View File

@@ -12,6 +12,5 @@ export { default as Box } from "./Box";
export { default as Button } from "./Button"; export { default as Button } from "./Button";
export { default as Pre } from "./Pre"; export { default as Pre } from "./Pre";
export { default as ButtonGroup } from "./ButtonGroup"; export { default as ButtonGroup } from "./ButtonGroup";
export { default as DeployFooter } from "./DeployFooter";
export * from "./Dialog"; export * from "./Dialog";
export * from "./DropdownMenu"; export * from "./DropdownMenu";

View File

@@ -40,9 +40,9 @@
{ {
"label": "Token", "label": "Token",
"body": { "body": {
"currency": "${1:13.1}", "currency": "${1:USD}",
"value": "${2:FOO}", "value": "${2:100}",
"description": "${3:rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpns}" "issuer": "${3:rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpns}"
} }
} }
] ]

View File

@@ -79,25 +79,40 @@
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100", "Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
"Fulfillment": "A0028000" "Fulfillment": "A0028000"
}, },
{
"TransactionType": "NFTokenMint",
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
"Fee": "10",
"NFTokenTaxon": 0,
"URI": "697066733A2F2F516D614374444B5A4656767666756676626479346573745A626851483744586831364354707631686F776D424779"
},
{ {
"TransactionType": "NFTokenBurn", "TransactionType": "NFTokenBurn",
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", "Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
"Fee": "10", "Fee": "10",
"TokenID": "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65" "NFTokenID": "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65"
}, },
{ {
"TransactionType": "NFTokenAcceptOffer", "TransactionType": "NFTokenAcceptOffer",
"Fee": "10" "Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
"Fee": "10",
"NFTokenSellOffer": "A2FA1A9911FE2AEF83DAB05F437768E26A301EF899BD31EB85E704B3D528FF18",
"NFTokenBuyOffer": "4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862",
"NFTokenBrokerFee": "10"
}, },
{ {
"TransactionType": "NFTokenCancelOffer", "TransactionType": "NFTokenCancelOffer",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", "Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"TokenIDs": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007" "Fee": "10",
"NFTokenOffers": {
"$type": "json",
"$value": ["4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862"]
}
}, },
{ {
"TransactionType": "NFTokenCreateOffer", "TransactionType": "NFTokenCreateOffer",
"Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX", "Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX",
"TokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007", "NFTokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
"Amount": { "Amount": {
"$value": "100", "$value": "100",
"$type": "xrp" "$type": "xrp"
@@ -212,9 +227,13 @@
"Fee": "12", "Fee": "12",
"Flags": 262144, "Flags": 262144,
"LastLedgerSequence": 8007750, "LastLedgerSequence": 8007750,
"Amount": { "LimitAmount": {
"$value": "100", "$type": "json",
"$type": "xrp" "$value": {
"currency": "USD",
"issuer": "rsP3mgGb2tcYUrxiLFiHJiQXhsziegtwBc",
"value": "100"
}
}, },
"Sequence": 12 "Sequence": 12
} }

View File

@@ -12,7 +12,7 @@
"dependencies": { "dependencies": {
"@codingame/monaco-jsonrpc": "^0.3.1", "@codingame/monaco-jsonrpc": "^0.3.1",
"@codingame/monaco-languageclient": "^0.17.0", "@codingame/monaco-languageclient": "^0.17.0",
"@monaco-editor/react": "^4.4.1", "@monaco-editor/react": "^4.4.5",
"@octokit/core": "^3.5.1", "@octokit/core": "^3.5.1",
"@radix-ui/colors": "^0.1.7", "@radix-ui/colors": "^0.1.7",
"@radix-ui/react-alert-dialog": "^0.1.1", "@radix-ui/react-alert-dialog": "^0.1.1",
@@ -23,8 +23,9 @@
"@radix-ui/react-popover": "^0.1.6", "@radix-ui/react-popover": "^0.1.6",
"@radix-ui/react-switch": "^0.1.5", "@radix-ui/react-switch": "^0.1.5",
"@radix-ui/react-tooltip": "^0.1.7", "@radix-ui/react-tooltip": "^0.1.7",
"@stitches/react": "^1.2.6-0", "@stitches/react": "^1.2.8",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"comment-parser": "^1.3.1",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"filesize": "^8.0.7", "filesize": "^8.0.7",
@@ -35,6 +36,7 @@
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"next": "^12.0.4", "next": "^12.0.4",
"next-auth": "^4.0.0-beta.5", "next-auth": "^4.0.0-beta.5",
"next-plausible": "^3.2.0",
"next-themes": "^0.1.1", "next-themes": "^0.1.1",
"normalize-url": "^7.0.2", "normalize-url": "^7.0.2",
"octokit": "^1.7.0", "octokit": "^1.7.0",
@@ -59,7 +61,7 @@
"vscode-languageserver": "^7.0.0", "vscode-languageserver": "^7.0.0",
"vscode-uri": "^3.0.2", "vscode-uri": "^3.0.2",
"wabt": "1.0.16", "wabt": "1.0.16",
"xrpl-accountlib": "^1.3.2", "xrpl-accountlib": "^1.5.2",
"xrpl-client": "^1.9.4" "xrpl-client": "^1.9.4"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -7,6 +7,7 @@ import { ThemeProvider } from "next-themes";
import { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { IdProvider } from "@radix-ui/react-id"; import { IdProvider } from "@radix-ui/react-id";
import PlausibleProvider from "next-plausible";
import { darkTheme, css } from "../stitches.config"; import { darkTheme, css } from "../stitches.config";
import Navigation from "../components/Navigation"; import Navigation from "../components/Navigation";
@@ -17,6 +18,8 @@ import TimeAgo from "javascript-time-ago";
import en from "javascript-time-ago/locale/en.json"; import en from "javascript-time-ago/locale/en.json";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import Alert from "../components/AlertDialog"; import Alert from "../components/AlertDialog";
import { Button, Flex } from "../components";
import { ChatCircleText } from "phosphor-react";
TimeAgo.setDefaultLocale(en.locale); TimeAgo.setDefaultLocale(en.locale);
TimeAgo.addLocale(en); TimeAgo.addLocale(en);
@@ -37,7 +40,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
if ( if (
!gistId && !gistId &&
router.isReady && router.isReady &&
!router.pathname.includes("/sign-in") && router.pathname.includes("/develop") &&
!snap.files.length && !snap.files.length &&
!snap.mainModalShowed !snap.mainModalShowed
) { ) {
@@ -114,6 +117,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
media="(prefers-color-scheme: light)" media="(prefers-color-scheme: light)"
/> />
</Head> </Head>
<IdProvider> <IdProvider>
<SessionProvider session={session}> <SessionProvider session={session}>
<ThemeProvider <ThemeProvider
@@ -125,23 +129,40 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
dark: darkTheme.className, dark: darkTheme.className,
}} }}
> >
<Navigation /> <PlausibleProvider
<Component {...pageProps} /> domain="hooks-builder.xrpl.org"
<Toaster trackOutboundLinks
toastOptions={{ >
className: css({ <Navigation />
backgroundColor: "$mauve1", <Component {...pageProps} />
color: "$mauve10", <Toaster
fontSize: "$sm", toastOptions={{
zIndex: 9999, className: css({
".dark &": { backgroundColor: "$mauve1",
backgroundColor: "$mauve4", color: "$mauve10",
color: "$mauve12", fontSize: "$sm",
}, zIndex: 9999,
})(), ".dark &": {
}} backgroundColor: "$mauve4",
/> color: "$mauve12",
<Alert /> },
})(),
}}
/>
<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> </ThemeProvider>
</SessionProvider> </SessionProvider>
</IdProvider> </IdProvider>

View File

@@ -1,8 +1,7 @@
import { Label } from "@radix-ui/react-label"; import { Label } from "@radix-ui/react-label";
import { Switch, SwitchThumb } from "../../components/Switch";
import type { NextPage } from "next"; import type { NextPage } from "next";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { Gear, Play } from "phosphor-react"; import { FileJs, Gear, Play } from "phosphor-react";
import Hotkeys from "react-hot-keys"; import Hotkeys from "react-hot-keys";
import Split from "react-split"; import Split from "react-split";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
@@ -10,6 +9,7 @@ import { ButtonGroup, Flex } from "../../components";
import Box from "../../components/Box"; import Box from "../../components/Box";
import Button from "../../components/Button"; import Button from "../../components/Button";
import Popover from "../../components/Popover"; import Popover from "../../components/Popover";
import RunScript from "../../components/RunScript";
import state from "../../state"; import state from "../../state";
import { compileCode } from "../../state/actions"; import { compileCode } from "../../state/actions";
import { getSplit, saveSplit } from "../../state/actions/persistSplits"; import { getSplit, saveSplit } from "../../state/actions/persistSplits";
@@ -141,59 +141,6 @@ const CompilerSettings = () => {
</Button> </Button>
</ButtonGroup> </ButtonGroup>
</Box> </Box>
<Box css={{ flexDirection: "column" }}>
<Label
style={{
flexDirection: "row",
display: "flex",
}}
>
Clean WASM (experimental){" "}
<Popover
css={{
maxWidth: "240px",
lineHeight: "1.3",
a: {
color: "$purple11",
},
".dark &": {
backgroundColor: "$black !important",
".arrow": {
fill: "$colors$black",
},
},
}}
content="Cleaner removes unwanted compiler-provided exports and functions from a wasm binary to make it (more) suitable for being used as a Hook"
>
<Flex
css={{
position: "relative",
top: "-1px",
mx: "$1",
backgroundColor: "$mauve8",
borderRadius: "$full",
cursor: "pointer",
width: "16px",
height: "16px",
alignItems: "center",
justifyContent: "center",
}}
>
?
</Flex>
</Popover>
</Label>
<Switch
css={{ mt: "$2" }}
checked={snap.compileOptions.strip}
onCheckedChange={(checked) => {
state.compileOptions.strip = checked;
}}
>
<SwitchThumb />
</Switch>
</Box>
</Flex> </Flex>
); );
}; };
@@ -213,7 +160,7 @@ const Home: NextPage = () => {
> >
<main style={{ display: "flex", flex: 1, position: "relative" }}> <main style={{ display: "flex", flex: 1, position: "relative" }}>
<HooksEditor /> <HooksEditor />
{snap.files[snap.active]?.name?.split(".")?.[1].toLowerCase() === {snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() ===
"c" && ( "c" && (
<Hotkeys <Hotkeys
keyName="command+b,ctrl+b" keyName="command+b,ctrl+b"
@@ -250,20 +197,61 @@ const Home: NextPage = () => {
</Flex> </Flex>
</Hotkeys> </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> </main>
<Box <Flex css={{ width: "100%" }}>
css={{ <Flex
display: "flex", css={{
background: "$mauve1", flex: 1,
position: "relative", background: "$mauve1",
}} position: "relative",
> borderRight: "1px solid $mauve8",
<LogBox }}
title="Development Log" >
clearLog={() => (state.logs = [])} <LogBox
logs={snap.logs} title="Development Log"
/> clearLog={() => (state.logs = [])}
</Box> 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> </Split>
); );
}; };

View File

@@ -6,6 +6,9 @@ import Transaction from "../../components/Transaction";
import state from "../../state"; import state from "../../state";
import { getSplit, saveSplit } from "../../state/actions/persistSplits"; import { getSplit, saveSplit } from "../../state/actions/persistSplits";
import { transactionsState, modifyTransaction } from "../../state"; import { transactionsState, modifyTransaction } from "../../state";
import { useEffect, useState } from "react";
import { FileJs } from "phosphor-react";
import RunScript from '../../components/RunScript';
const DebugStream = dynamic(() => import("../../components/DebugStream"), { const DebugStream = dynamic(() => import("../../components/DebugStream"), {
ssr: false, ssr: false,
@@ -19,14 +22,42 @@ const Accounts = dynamic(() => import("../../components/Accounts"), {
}); });
const Test = () => { const Test = () => {
// This and useEffect is here to prevent useLayoutEffect warnings from react-split
const [showComponent, setShowComponent] = useState(false);
const { transactionLogs } = useSnapshot(state); const { transactionLogs } = useSnapshot(state);
const { transactions, activeHeader } = useSnapshot(transactionsState); const { transactions, activeHeader } = useSnapshot(transactionsState);
const snap = useSnapshot(state);
useEffect(() => {
setShowComponent(true);
}, []);
if (!showComponent) {
return null;
}
const hasScripts = Boolean(
snap.files.filter(f => f.name.toLowerCase()?.endsWith(".js")).length
);
const renderNav = () => (
<Flex css={{ gap: "$3" }}>
{snap.files
.filter(f => f.name.endsWith(".js"))
.map(file => (
<RunScript file={file} key={file.name} />
))}
</Flex>
);
return ( return (
<Container css={{ px: 0 }}> <Container css={{ px: 0 }}>
<Split <Split
direction="vertical" direction="vertical"
sizes={getSplit("testVertical") || [50, 50]} sizes={
hasScripts && getSplit("testVertical")?.length === 2
? [50, 20, 30]
: hasScripts
? [50, 20, 50]
: [50, 50]
}
gutterSize={4} gutterSize={4}
gutterAlign="center" gutterAlign="center"
style={{ height: "calc(100vh - 60px)" }} style={{ height: "calc(100vh - 60px)" }}
@@ -56,14 +87,15 @@ const Test = () => {
> >
<Box css={{ width: "55%", px: "$2" }}> <Box css={{ width: "55%", px: "$2" }}>
<Tabs <Tabs
label="Transaction"
activeHeader={activeHeader} activeHeader={activeHeader}
// TODO make header a required field // TODO make header a required field
onChangeActive={(idx, header) => { onChangeActive={(idx, header) => {
if (header) transactionsState.activeHeader = header; if (header) transactionsState.activeHeader = header;
}} }}
keepAllAlive keepAllAlive
forceDefaultExtension defaultExtension="json"
defaultExtension=".json" allowedExtensions={["json"]}
onCreateNewTab={header => modifyTransaction(header, {})} onCreateNewTab={header => modifyTransaction(header, {})}
onCloseTab={(idx, header) => onCloseTab={(idx, header) =>
header && modifyTransaction(header, undefined) header && modifyTransaction(header, undefined)
@@ -71,10 +103,7 @@ const Test = () => {
> >
{transactions.map(({ header, state }) => ( {transactions.map(({ header, state }) => (
<Tab key={header} header={header}> <Tab key={header} header={header}>
<Transaction <Transaction state={state} header={header} />
state={state}
header={header}
/>
</Tab> </Tab>
))} ))}
</Tabs> </Tabs>
@@ -84,8 +113,25 @@ const Test = () => {
</Box> </Box>
</Split> </Split>
</Flex> </Flex>
{hasScripts ? (
<Flex row fluid> <Flex
as="div"
css={{
borderTop: "1px solid $mauve6",
background: "$mauve1",
flexDirection: "column",
}}
>
<LogBox
Icon={FileJs}
title="Helper scripts"
logs={snap.scriptLogs}
clearLog={() => (state.scriptLogs = [])}
renderNav={renderNav}
/>
</Flex>
) : null}
<Flex>
<Split <Split
direction="horizontal" direction="horizontal"
sizes={[50, 50]} sizes={[50, 50]}

View File

@@ -27,7 +27,7 @@ export const names = [
* new account with 10 000 XRP. Hooks Testnet /newcreds endpoint * new account with 10 000 XRP. Hooks Testnet /newcreds endpoint
* is protected with CORS so that's why we did our own endpoint * is protected with CORS so that's why we did our own endpoint
*/ */
export const addFaucetAccount = async (showToast: boolean = false) => { export const addFaucetAccount = async (name?: string, showToast: boolean = false) => {
// Lets limit the number of faucet accounts to 5 for now // Lets limit the number of faucet accounts to 5 for now
if (state.accounts.length > 5) { if (state.accounts.length > 5) {
return toast.error("You can only have maximum 6 accounts"); return toast.error("You can only have maximum 6 accounts");
@@ -52,7 +52,7 @@ export const addFaucetAccount = async (showToast: boolean = false) => {
} }
const currNames = state.accounts.map(acc => acc.name); const currNames = state.accounts.map(acc => acc.name);
state.accounts.push({ state.accounts.push({
name: names.filter(name => !currNames.includes(name))[0], name: name || names.filter(name => !currNames.includes(name))[0],
xrp: (json.xrp || 0 * 1000000).toString(), xrp: (json.xrp || 0 * 1000000).toString(),
address: json.address, address: json.address,
secret: json.secret, secret: json.secret,

View File

@@ -14,19 +14,21 @@ import { ref } from "valtio";
*/ */
export const compileCode = async (activeId: number) => { export const compileCode = async (activeId: number) => {
// Save the file to global state // Save the file to global state
saveFile(false); saveFile(false, activeId);
if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) { if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) {
throw Error("Missing env!"); throw Error("Missing env!");
} }
// Bail out if we're already compiling // Bail out if we're already compiling
if (state.compiling) { if (state.compiling) {
// if compiling is ongoing return // if compiling is ongoing return // TODO Inform user about it.
return; return;
} }
// Set loading state to true // Set loading state to true
state.compiling = true; state.compiling = true;
state.logs = [] state.logs = []
const file = state.files[activeId]
try { try {
file.containsErrors = false
const res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, { const res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
method: "POST", method: "POST",
headers: { headers: {
@@ -39,9 +41,9 @@ export const compileCode = async (activeId: number) => {
files: [ files: [
{ {
type: "c", type: "c",
options: state.compileOptions.optimizationLevel || '-O0', options: state.compileOptions.optimizationLevel || '-O2',
name: state.files[activeId].name, name: file.name,
src: state.files[activeId].content, src: file.content,
}, },
], ],
}), }),
@@ -49,15 +51,15 @@ export const compileCode = async (activeId: number) => {
const json = await res.json(); const json = await res.json();
state.compiling = false; state.compiling = false;
if (!json.success) { if (!json.success) {
state.logs.push({ type: "error", message: json.message }); const errors = [json.message]
if (json.tasks && json.tasks.length > 0) { if (json.tasks && json.tasks.length > 0) {
json.tasks.forEach((task: any) => { json.tasks.forEach((task: any) => {
if (!task.success) { if (!task.success) {
state.logs.push({ type: "error", message: task?.console }); errors.push(task?.console)
} }
}); });
} }
return toast.error(`Couldn't compile!`, { position: "bottom-center" }); throw errors
} }
state.logs.push({ state.logs.push({
type: "success", type: "success",
@@ -67,8 +69,9 @@ export const compileCode = async (activeId: number) => {
}); });
// Decode base64 encoded wasm that is coming back from the endpoint // Decode base64 encoded wasm that is coming back from the endpoint
const bufferData = await decodeBinary(json.output); const bufferData = await decodeBinary(json.output);
state.files[state.active].compiledContent = ref(bufferData); file.compiledContent = ref(bufferData);
state.files[state.active].lastCompiled = new Date(); file.lastCompiled = new Date();
file.compiledValueSnapshot = file.content
// Import wabt from and create human readable version of wasm file and // Import wabt from and create human readable version of wasm file and
// put it into state // put it into state
import("wabt").then((wabt) => { import("wabt").then((wabt) => {
@@ -84,10 +87,23 @@ export const compileCode = async (activeId: number) => {
}); });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
state.logs.push({
type: "error", if (err instanceof Array && typeof err[0] === 'string') {
message: "Error occured while compiling!", err.forEach(message => {
}); state.logs.push({
type: "error",
message,
});
})
}
else {
state.logs.push({
type: "error",
message: "Something went wrong, check your connection try again later!",
});
}
state.compiling = false; state.compiling = false;
toast.error(`Error occurred while compiling!`, { position: "bottom-center" });
file.containsErrors = true
} }
}; };

View File

@@ -54,15 +54,15 @@ export const prepareDeployHookTx = async (
account: IAccount & { name?: string }, account: IAccount & { name?: string },
data: SetHookData data: SetHookData
) => { ) => {
if ( const activeFile = state.files[state.active]?.compiledContent
!state.files || ? state.files[state.active]
state.files.length === 0 || : state.files.filter((file) => file.compiledContent)[0];
!state.files?.[state.active]?.compiledContent
) { if (!state.files || state.files.length === 0) {
return; return;
} }
if (!state.files?.[state.active]?.compiledContent) { if (!activeFile?.compiledContent) {
return; return;
} }
if (!state.client) { if (!state.client) {
@@ -99,7 +99,7 @@ export const prepareDeployHookTx = async (
{ {
Hook: { Hook: {
CreateCode: arrayBufferToHex( CreateCode: arrayBufferToHex(
state.files?.[state.active]?.compiledContent activeFile?.compiledContent
).toUpperCase(), ).toUpperCase(),
HookOn: calculateHookOn(hookOnValues), HookOn: calculateHookOn(hookOnValues),
HookNamespace, HookNamespace,
@@ -126,6 +126,10 @@ export const deployHook = async (
data: SetHookData data: SetHookData
) => { ) => {
if (typeof window !== "undefined") { 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); const tx = await prepareDeployHookTx(account, data);
if (!tx) { if (!tx) {
return; return;
@@ -160,14 +164,14 @@ export const deployHook = async (
message: ref( message: ref(
<> <>
[{submitRes.engine_result}] {submitRes.engine_result_message}{" "} [{submitRes.engine_result}] {submitRes.engine_result_message}{" "}
Validated ledger index:{" "} Transaction hash:{" "}
<Link <Link
as="a" as="a"
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${submitRes.validated_ledger_index}`} href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${submitRes.tx_json?.hash}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{submitRes.validated_ledger_index} {submitRes.tx_json?.hash}
</Link> </Link>
</> </>
), ),
@@ -185,7 +189,7 @@ export const deployHook = async (
console.log(err); console.log(err);
state.deployLogs.push({ state.deployLogs.push({
type: "error", type: "error",
message: "Error occured while deploying", message: "Error occurred while deploying",
}); });
} }
if (currentAccount) { if (currentAccount) {
@@ -268,10 +272,10 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
} }
} catch (err) { } catch (err) {
console.log(err); console.log(err);
toast.error("Error occured while deleting hoook", { id: toastId }); toast.error("Error occurred while deleting hook", { id: toastId });
state.deployLogs.push({ state.deployLogs.push({
type: "error", type: "error",
message: "Error occured while deleting hook", message: "Error occurred while deleting hook",
}); });
} }
if (currentAccount) { if (currentAccount) {

View File

@@ -8,11 +8,12 @@ export const downloadAsZip = async () => {
state.zipLoading = true state.zipLoading = true
// TODO do something about file/gist loading state // TODO do something about file/gist loading state
const files = state.files.map(({ name, content }) => ({ name, content })); const files = state.files.map(({ name, content }) => ({ name, content }));
const zipped = await createZip(files); const wasmFiles = state.files.filter(i => i.compiledContent).map(({ name, compiledContent }) => ({ name: `${name}.wasm`, content: compiledContent }));
const zipped = await createZip([...files, ...wasmFiles]);
const zipFileName = guessZipFileName(files); const zipFileName = guessZipFileName(files);
zipped.saveFile(zipFileName); zipped.saveFile(zipFileName);
} catch (error) { } 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 { } finally {
state.zipLoading = false state.zipLoading = false
} }

View File

@@ -19,20 +19,22 @@ export const fetchFiles = (gistId: string) => {
octokit octokit
.request("GET /gists/{gist_id}", { gist_id: gistId }) .request("GET /gists/{gist_id}", { gist_id: gistId })
.then(async res => { .then(async res => {
if (!Object.values(templateFileIds).includes(gistId)) { if (!Object.values(templateFileIds).map(v => v.id).includes(gistId)) {
return res return res
} }
// in case of templates, fetch header file(s) and append to res // in case of templates, fetch header file(s) and append to res
let resHeaderJson;
try { try {
const resHeader = await fetch(`${process.env.NEXT_PUBLIC_COMPILE_API_BASE_URL}/api/header-files`); const resHeader = await fetch(`${process.env.NEXT_PUBLIC_COMPILE_API_BASE_URL}/api/header-files`);
if (resHeader.ok) { if (resHeader.ok) {
resHeaderJson = await resHeader.json(); 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 = { const files = {
...res.data.files, ...res.data.files,
'hookapi.h': res.data.files?.['hookapi.h'] || { filename: 'hookapi.h', content: resHeaderJson.hookapi, language: 'C' }, ...headerFiles
'hookmacro.h': res.data.files?.['hookmacro.h'] || { filename: 'hookmacro.h', content: resHeaderJson.hookmacro, language: 'C' },
'sfcodes.h': res.data.files?.['sfcodes.h'] || { filename: 'sfcodes.h', content: resHeaderJson.sfcodes, language: 'C' },
}; };
res.data.files = files; res.data.files = files;
} }
@@ -58,6 +60,29 @@ export const fetchFiles = (gistId: string) => {
language: res.data.files?.[filename]?.language?.toLowerCase() || "", language: res.data.files?.[filename]?.language?.toLowerCase() || "",
content: res.data.files?.[filename]?.content || "", 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; state.loading = false;
if (files.length > 0) { if (files.length > 0) {
state.logs.push({ state.logs.push({
@@ -89,4 +114,4 @@ export const fetchFiles = (gistId: string) => {
return; return;
} }
state.loading = false; state.loading = false;
}; };

View File

@@ -5,7 +5,7 @@ import state from '../index';
import { names } from './addFaucetAccount'; import { names } from './addFaucetAccount';
// Adds test account to global state with secret key // Adds test account to global state with secret key
export const importAccount = (secret: string) => { export const importAccount = (secret: string, name?: string) => {
if (!secret) { if (!secret) {
return toast.error("You need to add secret!"); return toast.error("You need to add secret!");
} }
@@ -19,7 +19,7 @@ export const importAccount = (secret: string) => {
if (err?.message) { if (err?.message) {
toast.error(err.message) toast.error(err.message)
} else { } else {
toast.error('Error occured while importing account') toast.error('Error occurred while importing account')
} }
return; return;
} }
@@ -27,7 +27,7 @@ export const importAccount = (secret: string) => {
return toast.error(`Couldn't create account!`); return toast.error(`Couldn't create account!`);
} }
state.accounts.push({ state.accounts.push({
name: names[state.accounts.length], name: name || names[state.accounts.length],
address: account.address || "", address: account.address || "",
secret: account.secret.familySeed || "", secret: account.secret.familySeed || "",
xrp: "0", xrp: "0",

View File

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

View File

@@ -20,11 +20,10 @@ export const sendTransaction = async (account: IAccount, txOptions: TransactionO
const { Fee = "1000", ...opts } = txOptions const { Fee = "1000", ...opts } = txOptions
const tx: TransactionOptions = { const tx: TransactionOptions = {
Account: account.address, Account: account.address,
Sequence: account.sequence, // TODO auto-fillable Sequence: account.sequence,
Fee, // TODO auto-fillable Fee, // TODO auto-fillable default
...opts ...opts
}; };
const { logPrefix = '' } = options || {} const { logPrefix = '' } = options || {}
try { try {
const signedAccount = derive.familySeed(account.secret); const signedAccount = derive.familySeed(account.secret);

View File

@@ -1,20 +1,41 @@
// export const templateFileIds = { import Carbon from "../../components/icons/Carbon";
// 'starter': '1d14e51e2e02dc0a508cb0733767a914', // TODO currently same as accept import Firewall from "../../components/icons/Firewall";
// 'firewall': 'bcd6d0c0fcbe52545ddb802481ff9d26', import Notary from "../../components/icons/Notary";
// 'notary': 'a789c75f591eeab7932fd702ed8cf9ea', import Peggy from "../../components/icons/Peggy";
// 'carbon': '43925143fa19735d8c6505c34d3a6a47', import Starter from "../../components/icons/Starter";
// 'peggy': 'ceaf352e2a65741341033ab7ef05c448',
// 'headers': '9b448e8a55fab11ef5d1274cb59f9cf3'
// }
export const templateFileIds = { export const templateFileIds = {
'starter': '1f7d2963d9e342ea092286115274f3e3', 'starter': {
'firewall': '70edec690f0de4dd315fad1f4f996d8c', id: '9106f1fe60482d90475bfe8f1315affe',
'notary': '3d5677768fe8a54c4f6317e185d9ba66', name: 'Starter',
'carbon': 'a9fbcaf1b816b198c7fc0f62962bebf2', description: 'Just a basic starter with essential imports, just accepts any transaction coming through',
'doubler': '56b86174aeb70b2b48eee962bad3e355', icon: Starter
'peggy': 'd21298a37e1550b781682014762a567b',
'headers': '55f639bce59a49c58c45e663776b5138' },
'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'];

View File

@@ -13,9 +13,11 @@ export interface IFile {
name: string; name: string;
language: string; language: string;
content: string; content: string;
compiledValueSnapshot?: string
compiledContent?: ArrayBuffer | null; compiledContent?: ArrayBuffer | null;
compiledWatContent?: string | null; compiledWatContent?: string | null;
lastCompiled?: Date lastCompiled?: Date
containsErrors?: boolean
} }
export interface FaucetAccountRes { export interface FaucetAccountRes {
@@ -52,6 +54,8 @@ export interface ILog {
defaultCollapsed?: boolean defaultCollapsed?: boolean
} }
export type DeployValue = Record<IFile['name'], any>;
export interface IState { export interface IState {
files: IFile[]; files: IFile[];
gistId?: string | null; gistId?: string | null;
@@ -66,6 +70,7 @@ export interface IState {
logs: ILog[]; logs: ILog[];
deployLogs: ILog[]; deployLogs: ILog[];
transactionLogs: ILog[]; transactionLogs: ILog[];
scriptLogs: ILog[];
editorCtx?: typeof monaco.editor; editorCtx?: typeof monaco.editor;
editorSettings: { editorSettings: {
tabSize: number; tabSize: number;
@@ -81,7 +86,8 @@ export interface IState {
compileOptions: { compileOptions: {
optimizationLevel: '-O0' | '-O1' | '-O2' | '-O3' | '-O4' | '-Os'; optimizationLevel: '-O0' | '-O1' | '-O2' | '-O3' | '-O4' | '-Os';
strip: boolean strip: boolean
} },
deployValues: DeployValue
} }
// let localStorageState: null | string = null; // let localStorageState: null | string = null;
@@ -96,6 +102,7 @@ let initialState: IState = {
logs: [], logs: [],
deployLogs: [], deployLogs: [],
transactionLogs: [], transactionLogs: [],
scriptLogs: [],
editorCtx: undefined, editorCtx: undefined,
gistId: undefined, gistId: undefined,
gistOwner: undefined, gistOwner: undefined,
@@ -112,9 +119,10 @@ let initialState: IState = {
mainModalShowed: false, mainModalShowed: false,
accounts: [], accounts: [],
compileOptions: { compileOptions: {
optimizationLevel: '-O0', optimizationLevel: '-O2',
strip: true strip: true
} },
deployValues: {}
}; };
let localStorageAccounts: string | null = null; let localStorageAccounts: string | null = null;

View File

@@ -19,7 +19,8 @@ export interface TransactionState {
txFields: TxFields; txFields: TxFields;
viewType: 'json' | 'ui', viewType: 'json' | 'ui',
editorSavedValue: null | string, editorSavedValue: null | string,
editorValue?: string editorValue?: string,
estimatedFee?: string
} }
@@ -93,7 +94,7 @@ export const modifyTransaction = (
Object.keys(partialTx).forEach(k => { Object.keys(partialTx).forEach(k => {
// Typescript mess here, but is definetly safe! // Typescript mess here, but is definetly safe!
const s = tx.state as any; const s = tx.state as any;
const p = partialTx as any; const p = partialTx as any; // ? Make copy
if (!deepEqual(s[k], p[k])) s[k] = p[k]; if (!deepEqual(s[k], p[k])) s[k] = p[k];
}); });
@@ -117,7 +118,7 @@ export const prepareTransaction = (data: any) => {
// handle type: `json` // handle type: `json`
if (_value && typeof _value === "object" && _value.$type === "json") { if (_value && typeof _value === "object" && _value.$type === "json") {
if (typeof _value.$value === "object") { if (typeof _value.$value === "object") {
options[field] = _value.$value as any; options[field] = _value.$value;
} else { } else {
try { try {
options[field] = JSON.parse(_value.$value); options[field] = JSON.parse(_value.$value);
@@ -130,7 +131,7 @@ export const prepareTransaction = (data: any) => {
} }
} }
// delete unneccesary fields // delete unnecessary fields
if (options[field] === undefined) { if (options[field] === undefined) {
delete options[field]; delete options[field];
} }
@@ -140,7 +141,7 @@ export const prepareTransaction = (data: any) => {
} }
// editor value to state // editor value to state
export const prepareState = (value: string, txState: TransactionState) => { export const prepareState = (value: string, transactionType?: string) => {
const options = parseJSON(value); const options = parseJSON(value);
if (!options) { if (!options) {
showAlert("Error!", { showAlert("Error!", {
@@ -151,7 +152,7 @@ export const prepareState = (value: string, txState: TransactionState) => {
const { Account, TransactionType, Destination, ...rest } = options; const { Account, TransactionType, Destination, ...rest } = options;
let tx: Partial<TransactionState> = {}; let tx: Partial<TransactionState> = {};
const { txFields } = txState const txFields = getTxFields(transactionType)
if (Account) { if (Account) {
const acc = state.accounts.find(acc => acc.address === Account); const acc = state.accounts.find(acc => acc.address === Account);
@@ -206,7 +207,7 @@ export const prepareState = (value: string, txState: TransactionState) => {
if (isXrp) { if (isXrp) {
rest[field] = { rest[field] = {
$type: "xrp", $type: "xrp",
$value: +value / 1000000, // TODO maybe use bigint? $value: +value / 1000000, // ! maybe use bigint?
}; };
} else if (typeof value === "object") { } else if (typeof value === "object") {
rest[field] = { rest[field] = {
@@ -222,4 +223,24 @@ export const prepareState = (value: string, txState: TransactionState) => {
return tx 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 { transactionsData }

24
utils/comment-parser.ts Normal file
View 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
}

View File

@@ -1,24 +1,29 @@
import toast from 'react-hot-toast';
import { derive, sign } from "xrpl-accountlib" import { derive, sign } from "xrpl-accountlib"
import state, { IAccount } from "../state" import state, { IAccount } from "../state"
const estimateFee = async (tx: Record<string, unknown>, account: IAccount): Promise<null | { base_fee: string, median_fee: string; minimum_fee: string; open_ledger_fee: string; }> => { 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; }> => {
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);
try { 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 }) const res = await state.client?.send({ command: 'fee', tx_blob: signedTransaction })
if (res && res.drops) { if (res && res.drops) {
return res.drops; return res.drops;
} }
return null return null
} catch (err) { } catch (err) {
console.log(err) if (!opts.silent) {
console.error(err)
toast.error("Cannot estimate fee.") // ? Some better msg
}
return null return null
} }
} }

View File

@@ -6,4 +6,10 @@ export const guessZipFileName = (files: File[]) => {
let parts = (files.filter(f => f.name.endsWith('.c'))[0]?.name || 'hook').split('.') let parts = (files.filter(f => f.name.endsWith('.c'))[0]?.name || 'hook').split('.')
parts = parts.length > 1 ? parts.slice(0, -1) : parts parts = parts.length > 1 ? parts.slice(0, -1) : parts
return parts.join('') return parts.join('')
}
export const capitalize = (value?: string) => {
if (!value) return '';
return value[0].toLocaleUpperCase() + value.slice(1);
} }

View File

@@ -18,13 +18,18 @@ export const tts = {
ttDEPOSIT_PREAUTH: 19, ttDEPOSIT_PREAUTH: 19,
ttTRUST_SET: 20, ttTRUST_SET: 20,
ttACCOUNT_DELETE: 21, ttACCOUNT_DELETE: 21,
ttHOOK_SET: 22 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; export type TTS = typeof tts;
const calculateHookOn = (arr: (keyof TTS)[]) => { const calculateHookOn = (arr: (keyof TTS)[]) => {
let start = '0x00000000003ff5bf'; let start = '0x000000003e3ff5bf';
arr.forEach(n => { arr.forEach(n => {
let v = BigInt(start); let v = BigInt(start);
v ^= (BigInt(1) << BigInt(tts[n as keyof TTS])); v ^= (BigInt(1) << BigInt(tts[n as keyof TTS]));

View File

@@ -1,6 +1,5 @@
import { MessageConnection } from "@codingame/monaco-jsonrpc"; import { MessageConnection } from "@codingame/monaco-jsonrpc";
import { MonacoLanguageClient, ErrorAction, CloseAction, createConnection } from "@codingame/monaco-languageclient"; import { MonacoLanguageClient, ErrorAction, CloseAction, createConnection } from "@codingame/monaco-languageclient";
import Router from "next/router";
import normalizeUrl from "normalize-url"; import normalizeUrl from "normalize-url";
import ReconnectingWebSocket from "reconnecting-websocket"; import ReconnectingWebSocket from "reconnecting-websocket";
@@ -14,11 +13,7 @@ export function createLanguageClient(connection: MessageConnection): MonacoLangu
errorHandler: { errorHandler: {
error: () => ErrorAction.Continue, error: () => ErrorAction.Continue,
closed: () => { closed: () => {
if (Router.pathname.includes('/develop')) { return CloseAction.DoNotRestart
return CloseAction.Restart
} else {
return CloseAction.DoNotRestart
}
} }
}, },

View File

@@ -41,6 +41,7 @@ import hooksSkipHashBufLen from "./md/hooks-skip-hash-buf-len.md";
import hooksStateBufLen from "./md/hooks-state-buf-len.md"; import hooksStateBufLen from "./md/hooks-state-buf-len.md";
import hooksTransactionHashBufLen from "./md/hooks-transaction-hash-buf-len.md"; import hooksTransactionHashBufLen from "./md/hooks-transaction-hash-buf-len.md";
import hooksTransactionSlotLimit from "./md/hooks-transaction-slot-limit.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 hooksValidateBufLen from "./md/hooks-validate-buf-len.md";
import hooksVerifyBufLen from "./md/hooks-verify-buf-len.md"; import hooksVerifyBufLen from "./md/hooks-verify-buf-len.md";
@@ -90,6 +91,7 @@ const docs: { [key: string]: string; } = {
"hooks-state-buf-len": hooksStateBufLen, "hooks-state-buf-len": hooksStateBufLen,
"hooks-transaction-hash-buf-len": hooksTransactionHashBufLen, "hooks-transaction-hash-buf-len": hooksTransactionHashBufLen,
"hooks-transaction-slot-limit": hooksTransactionSlotLimit, "hooks-transaction-slot-limit": hooksTransactionSlotLimit,
"hooks-trivial-cbak": hooksTrivialCbak,
"hooks-validate-buf-len": hooksValidateBufLen, "hooks-validate-buf-len": hooksValidateBufLen,
"hooks-verify-buf-len": hooksVerifyBufLen, "hooks-verify-buf-len": hooksVerifyBufLen,
}; };

View File

@@ -1,7 +1,7 @@
# hooks-entry-points # hooks-entry-points
A Hook always implements and exports exactly two functions: [cbak](https://xrpl-hooks.readme.io/v2.0/reference/cbak) and [hook](https://xrpl-hooks.readme.io/v2.0/reference/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) [Read more](https://xrpl-hooks.readme.io/v2.0/docs/compiling-hooks)

View File

@@ -1,5 +1,5 @@
# hooks-hash-buf-len # hooks-hash-buf-len
Functions [util_sha512h](https://xrpl-hooks.readme.io/v2.0/reference/util_sha512h), [hook_hash](https://xrpl-hooks.readme.io/v2.0/reference/hook_hash), [ledger_last_hash](https://xrpl-hooks.readme.io/v2.0/reference/ledger_last_hash) and [nonce](https://xrpl-hooks.readme.io/v2.0/reference/nonce) have fixed-size hash output. Functions [util_sha512h](https://xrpl-hooks.readme.io/v2.0/reference/util_sha512h), [hook_hash](https://xrpl-hooks.readme.io/v2.0/reference/hook_hash), [ledger_last_hash](https://xrpl-hooks.readme.io/v2.0/reference/ledger_last_hash), [etxn_nonce](https://xrpl-hooks.readme.io/v2.0/reference/etxn_nonce) and [ledger_nonce](https://xrpl-hooks.readme.io/v2.0/reference/ledger_nonce) have fixed-size hash output.
This check warns about too-small size of their output buffer (if it's specified by a constant - variable parameter is ignored). This check warns about too-small size of their output buffer (if it's specified by a constant - variable parameter is ignored).

View File

@@ -0,0 +1,7 @@
# hooks-trivial-cbak
A Hook may implement and export a [cbak](https://xrpl-hooks.readme.io/v2.0/reference/cbak) function.
But the function is optional, and defining it so that it doesn't do anything besides returning a constant value is unnecessary (except for some debugging scenarios) and just increases the hook size. This check warns about such implementations.
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/compiling-hooks)

109
yarn.lock
View File

@@ -202,19 +202,19 @@
resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@monaco-editor/loader@^1.3.0": "@monaco-editor/loader@^1.3.2":
version "1.3.0" version "1.3.2"
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.3.0.tgz#659fbaf1d612ea67b2a0519a18612d1c4813e444" resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.3.2.tgz#04effbb87052d19cd7d3c9d81c0635490f9bb6d8"
integrity sha512-N3mGq1ktC3zh7WUx3NGO+PSDdNq9Vspk/41rEmRdrCqV9vNbBTRzAOplmUpNQsi+hmTs++ERMBobMERb8Kb+3g== integrity sha512-BTDbpHl3e47r3AAtpfVFTlAi7WXv4UQ/xZmz8atKl4q7epQV5e7+JbigFDViWF71VBi4IIBdcWP57Hj+OWuc9g==
dependencies: dependencies:
state-local "^1.0.6" state-local "^1.0.6"
"@monaco-editor/react@^4.4.1": "@monaco-editor/react@^4.4.5":
version "4.4.1" version "4.4.5"
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.4.1.tgz#2e2b9b369f3082b0e14f47cdbe35658fd56c7c7d" resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.4.5.tgz#beabe491efeb2457441a00d1c7651c653697f65b"
integrity sha512-95E/XPC4dbm/7qdkhSsU/a1kRgcn2PYhRTVIc+/cixWCZrwRURW1DRPaIZ2lOawBJ6kAOLywxuD4A4UmbT0ZIw== integrity sha512-IImtzU7sRc66OOaQVCG+5PFHkSWnnhrUWGBuH6zNmH2h0YgmAhcjHZQc/6MY9JWEbUtVF1WPBMJ9u1XuFbRrVA==
dependencies: dependencies:
"@monaco-editor/loader" "^1.3.0" "@monaco-editor/loader" "^1.3.2"
prop-types "^15.7.2" prop-types "^15.7.2"
"@next/env@12.1.0": "@next/env@12.1.0":
@@ -917,10 +917,10 @@
resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.0.tgz" resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.0.tgz"
integrity sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A== integrity sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A==
"@stitches/react@^1.2.6-0": "@stitches/react@^1.2.8":
version "1.2.7" version "1.2.8"
resolved "https://registry.npmjs.org/@stitches/react/-/react-1.2.7.tgz" resolved "https://registry.yarnpkg.com/@stitches/react/-/react-1.2.8.tgz#954f8008be8d9c65c4e58efa0937f32388ce3a38"
integrity sha512-6AxpUag7OW55ANzRnuy7R15FEyQeZ66fytVo3BBilFIU0mfo3t49CAMcEAL/A1SbhSj/FCdWkn/XrbjGBTJTzg== integrity sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA==
"@types/aws-lambda@^8.10.83": "@types/aws-lambda@^8.10.83":
version "8.10.93" version "8.10.93"
@@ -1280,14 +1280,6 @@ babel-plugin-macros@^2.6.1:
cosmiconfig "^6.0.0" cosmiconfig "^6.0.0"
resolve "^1.12.0" resolve "^1.12.0"
babel-runtime@^6.26.0:
version "6.26.0"
resolved "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz"
integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
dependencies:
core-js "^2.4.0"
regenerator-runtime "^0.11.0"
balanced-match@^1.0.0: balanced-match@^1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
@@ -1525,6 +1517,11 @@ color-name@~1.1.4:
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
comment-parser@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.3.1.tgz#3d7ea3adaf9345594aedee6563f422348f165c1b"
integrity sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==
concat-map@0.0.1: concat-map@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
@@ -1547,11 +1544,6 @@ core-js-pure@^3.20.2:
resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.21.1.tgz" resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.21.1.tgz"
integrity sha512-12VZfFIu+wyVbBebyHmRTuEE/tZrB4tJToWcwAMcsp3h4+sHR+fMJWbKpYiCRWlhFBq+KNyO8rIV9rTkeVmznQ== integrity sha512-12VZfFIu+wyVbBebyHmRTuEE/tZrB4tJToWcwAMcsp3h4+sHR+fMJWbKpYiCRWlhFBq+KNyO8rIV9rTkeVmznQ==
core-js@^2.4.0:
version "2.6.12"
resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
core-util-is@~1.0.0: core-util-is@~1.0.0:
version "1.0.3" version "1.0.3"
resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz"
@@ -2964,6 +2956,11 @@ next-auth@^4.0.0-beta.5:
preact-render-to-string "^5.1.19" preact-render-to-string "^5.1.19"
uuid "^8.3.2" uuid "^8.3.2"
next-plausible@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/next-plausible/-/next-plausible-3.2.0.tgz#d801346253e0c1cf64a02b9fc3a42050455cbc47"
integrity sha512-OlYcLXBG3kKd/fKMpm8SZ5IkUKSFm1/8t7cv6e5bewIqlpdZpdWuSrjbdJpbmutb2KPLXHzilKp09zmDGjy9KQ==
next-themes@^0.1.1: next-themes@^0.1.1:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.1.1.tgz#122113a458bf1d1be5ffed66778ab924c106f82a" resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.1.1.tgz#122113a458bf1d1be5ffed66778ab924c106f82a"
@@ -3535,11 +3532,6 @@ reconnecting-websocket@^4.4.0:
resolved "https://registry.npmjs.org/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz" resolved "https://registry.npmjs.org/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz"
integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng== integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==
regenerator-runtime@^0.11.0:
version "0.11.1"
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regenerator-runtime@^0.13.4: regenerator-runtime@^0.13.4:
version "0.13.9" version "0.13.9"
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz" resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz"
@@ -3638,20 +3630,15 @@ ripple-address-codec@^4.1.0, ripple-address-codec@^4.1.1, ripple-address-codec@^
base-x "3.0.9" base-x "3.0.9"
create-hash "^1.1.2" create-hash "^1.1.2"
ripple-binary-codec@^0.2.4: ripple-address-codec@^4.2.4:
version "0.2.7" version "4.2.4"
resolved "https://registry.npmjs.org/ripple-binary-codec/-/ripple-binary-codec-0.2.7.tgz" resolved "https://registry.yarnpkg.com/ripple-address-codec/-/ripple-address-codec-4.2.4.tgz#a56c2168c8bb81269ea4d15ed96d6824c5a866f8"
integrity sha512-VD+sHgZK76q3kmO765klFHPDCEveS5SUeg/bUNVpNrj7w2alyDNkbF17XNbAjFv+kSYhfsUudQanoaSs2Y6uzw== integrity sha512-roAOjKz94+FboTItey1XRh5qynwt4xvfBLvbbcx+FiR94Yw2x3LrKLF2GVCMCSAh5I6PkcpADg6AbYsUbGN3nA==
dependencies: dependencies:
babel-runtime "^6.26.0" base-x "3.0.9"
bn.js "^5.1.1" create-hash "^1.1.2"
create-hash "^1.2.0"
decimal.js "^10.2.0"
inherits "^2.0.4"
lodash "^4.17.15"
ripple-address-codec "^4.1.0"
ripple-binary-codec@^1.1.3, ripple-binary-codec@^1.3.0: ripple-binary-codec@^1.1.3:
version "1.3.2" version "1.3.2"
resolved "https://registry.npmjs.org/ripple-binary-codec/-/ripple-binary-codec-1.3.2.tgz" resolved "https://registry.npmjs.org/ripple-binary-codec/-/ripple-binary-codec-1.3.2.tgz"
integrity sha512-8VG1vfb3EM1J7ZdPXo9E57Zv2hF4cxT64gP6rGSQzODVgMjiBCWozhN3729qNTGtHItz0e82Oix8v95vWYBQ3A== integrity sha512-8VG1vfb3EM1J7ZdPXo9E57Zv2hF4cxT64gP6rGSQzODVgMjiBCWozhN3729qNTGtHItz0e82Oix8v95vWYBQ3A==
@@ -3663,6 +3650,18 @@ ripple-binary-codec@^1.1.3, ripple-binary-codec@^1.3.0:
decimal.js "^10.2.0" decimal.js "^10.2.0"
ripple-address-codec "^4.2.3" ripple-address-codec "^4.2.3"
ripple-binary-codec@^1.4.2:
version "1.4.2"
resolved "https://registry.yarnpkg.com/ripple-binary-codec/-/ripple-binary-codec-1.4.2.tgz#cdc35353e4bc7c3a704719247c82b4c4d0b57dd3"
integrity sha512-EDKIyZMa/6Ay/oNgCwjD9b9CJv0zmBreeHVQeG4BYwy+9GPnIQjNeT5e/aB6OjAnhcmpgbPeBmzwmNVwzxlt0w==
dependencies:
assert "^2.0.0"
big-integer "^1.6.48"
buffer "5.6.0"
create-hash "^1.2.0"
decimal.js "^10.2.0"
ripple-address-codec "^4.2.4"
ripple-bs58@^4.0.0: ripple-bs58@^4.0.0:
version "4.0.1" version "4.0.1"
resolved "https://registry.npmjs.org/ripple-bs58/-/ripple-bs58-4.0.1.tgz" resolved "https://registry.npmjs.org/ripple-bs58/-/ripple-bs58-4.0.1.tgz"
@@ -3686,7 +3685,7 @@ ripple-hashes@^0.3.4, ripple-hashes@latest:
bignumber.js "^4.1.0" bignumber.js "^4.1.0"
create-hash "^1.1.2" create-hash "^1.1.2"
ripple-address-codec "^3.0.4" ripple-address-codec "^3.0.4"
ripple-binary-codec "^0.2.4" ripple-binary-codec "^1.4.2"
ripple-keypairs@^1.0.3, ripple-keypairs@latest: ripple-keypairs@^1.0.3, ripple-keypairs@latest:
version "1.1.3" version "1.1.3"
@@ -4318,10 +4317,10 @@ ws@^7.2.0:
resolved "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz" resolved "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz"
integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A== integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==
xrpl-accountlib@^1.3.2: xrpl-accountlib@^1.5.2:
version "1.3.2" version "1.5.2"
resolved "https://registry.npmjs.org/xrpl-accountlib/-/xrpl-accountlib-1.3.2.tgz" resolved "https://registry.yarnpkg.com/xrpl-accountlib/-/xrpl-accountlib-1.5.2.tgz#8f16abe449fd60ba9ed75597f6ce3f0c45dfff43"
integrity sha512-mXwoumGp0xUiZ7Ty/1o4FHVRK4uLnqngxdYmikZs50drMjlgCUP6GXun2Vf4Uus1fnVnxhXIw+E7peH5OjiOJA== integrity sha512-lieY2/5G9DySqdtgQ0AD/aMMG5Sy/MLAmbIsmsCaF06scM5DpR8s4SsEzgHni7dOG68Wjnb2Uz6tf5aV+l4/Kg==
dependencies: dependencies:
assert "^2.0.0" assert "^2.0.0"
bip32 "^2.0.5" bip32 "^2.0.5"
@@ -4330,13 +4329,13 @@ xrpl-accountlib@^1.3.2:
elliptic "6.5.4" elliptic "6.5.4"
hash.js "^1.1.7" hash.js "^1.1.7"
ripple-address-codec "^4.1.0" ripple-address-codec "^4.1.0"
ripple-binary-codec "^1.3.0" ripple-binary-codec "^1.4.2"
ripple-hashes "^0.3.4" ripple-hashes "^0.3.4"
ripple-keypairs "^1.0.3" ripple-keypairs "^1.0.3"
ripple-lib "^1.6.4" ripple-lib "^1.6.4"
ripple-secret-codec "^1.0.2" ripple-secret-codec "^1.0.2"
xrpl-secret-numbers "^0.3.3" xrpl-secret-numbers "^0.3.3"
xrpl-sign-keypairs "^2.0.1" xrpl-sign-keypairs "^2.1.1"
xrpl-client@^1.9.4: xrpl-client@^1.9.4:
version "1.9.4" version "1.9.4"
@@ -4355,13 +4354,13 @@ xrpl-secret-numbers@^0.3.3:
brorand "^1.1.0" brorand "^1.1.0"
ripple-keypairs "^1.0.3" ripple-keypairs "^1.0.3"
xrpl-sign-keypairs@^2.0.1: xrpl-sign-keypairs@^2.1.1:
version "2.0.1" version "2.1.1"
resolved "https://registry.npmjs.org/xrpl-sign-keypairs/-/xrpl-sign-keypairs-2.0.1.tgz" resolved "https://registry.yarnpkg.com/xrpl-sign-keypairs/-/xrpl-sign-keypairs-2.1.1.tgz#2f7f2855799c5d4ba091007963825eef1db21a4e"
integrity sha512-84QbE3trxetaw0hqDADCWMx0HH1VAWnTJp0TGoKTGRf1jzTqjI7eNNNw5lmcay2MH8bW/waNzJIF8vSAJSkVrQ== integrity sha512-rKQmUCx+x7gjjJ5zv/Z7bOYR+8I36JwUCFlpuD9UzYD4w2msGQDG0rmxVENyZSfThDBVQ1kEArVn6SMDMe9LUQ==
dependencies: dependencies:
big-integer latest big-integer latest
ripple-binary-codec "^1.3.0" ripple-binary-codec "^1.4.2"
ripple-bs58check latest ripple-bs58check latest
ripple-hashes latest ripple-hashes latest
ripple-keypairs latest ripple-keypairs latest