Skip to content

Commit c3c19d3

Browse files
committed
add experiment and fixes
1 parent cd95502 commit c3c19d3

18 files changed

Lines changed: 2283 additions & 397 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
### [0.11.0](https://github.com/xdevguild/buildo.dev/releases/tag/v0.11.0) (2023-12-25)
2+
- add experimental inscriptions operation (the txData schema may change) read more at [MultiversX Agora](https://agora.multiversx.com/t/a-guide-for-builders-on-how-to-properly-create-and-manage-inscriptions-on-multiversx/303)
3+
- fix sign message when using with redirections (web wallet)
4+
- fix sign message using Ledger
5+
- add possibility to open the operation dialog with URL link
6+
- update dependencies
7+
18
### [0.10.0](https://github.com/xdevguild/buildo.dev/releases/tag/v0.10.0) (2023-12-01)
29
- add sign a message operation
310

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
'use client';
2+
3+
import { Button } from '@/components/ui/button';
4+
import { Spinner } from '@/components/ui/spinner';
5+
import { usePersistStorage } from '@/hooks/use-form-storage';
6+
import { TransactionPayload } from '@multiversx/sdk-core/out';
7+
import {
8+
useAccount,
9+
useConfig,
10+
useLoginInfo,
11+
useTransaction,
12+
} from '@useelven/core';
13+
14+
export const Broadcast = ({
15+
setNextStep,
16+
}: {
17+
setNextStep: (state: boolean) => void;
18+
}) => {
19+
const { address } = useAccount();
20+
const { loginMethod } = useLoginInfo();
21+
const { explorerAddress } = useConfig();
22+
const { triggerTx, pending, txResult, error } = useTransaction({
23+
webWalletRedirectUrl: '/inscriptions/create',
24+
});
25+
26+
const { storageValue: inscription } = usePersistStorage({
27+
storageItem: 'general-createInscription-inscription',
28+
});
29+
30+
const { storageValue: rawPayload } = usePersistStorage({
31+
storageItem: 'general-createInscription-partialPayload',
32+
});
33+
34+
const { storageValue: signature } = usePersistStorage({
35+
storageItem: 'general-createInscription-signature',
36+
});
37+
38+
const onSubmit = async () => {
39+
if (rawPayload && signature) {
40+
const payload = JSON.stringify({
41+
...rawPayload,
42+
signature,
43+
});
44+
45+
const data = new TransactionPayload(payload);
46+
47+
triggerTx?.({
48+
address,
49+
gasLimit: 50000 + 1500 * data.length(),
50+
data,
51+
value: 0,
52+
});
53+
}
54+
};
55+
56+
const getSigningProviderName = () => {
57+
if (loginMethod === 'walletconnect') {
58+
return 'xPortal';
59+
}
60+
return loginMethod;
61+
};
62+
63+
return (
64+
<div className="px-0 sm:px-8">
65+
<div className="mb-3 font-bold">
66+
{txResult?.isCompleted && (
67+
<div>
68+
Your inscription was broadcasted. You can now find it{' '}
69+
<a
70+
href={`${explorerAddress}/transactions/${txResult.hash}`}
71+
target="_blank"
72+
className="underline"
73+
>
74+
on-chain
75+
</a>
76+
.
77+
</div>
78+
)}
79+
{!txResult?.isCompleted && signature && (
80+
<div>Your inscription was signed. Now You can broadcast it!</div>
81+
)}
82+
{error && <div>There was an error: {error}</div>}
83+
</div>
84+
<div className="mb-3">
85+
{pending && (
86+
<div className="font-bold flex items-center gap-3">
87+
<Spinner size={20} /> Transaction pending (confirmation through{' '}
88+
{getSigningProviderName()})...
89+
</div>
90+
)}
91+
{!pending && inscription && (
92+
<code className="bg-slate-100 dark:bg-slate-800 dark:text-slate-50 py-4 px-6 block max-h-96 overflow-auto break-all">
93+
{inscription}
94+
</code>
95+
)}
96+
</div>
97+
<div className="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 py-4 px-8">
98+
{txResult?.isCompleted || error ? (
99+
<Button size="sm" onClick={() => setNextStep(false)}>
100+
{error ? 'Try again' : 'Sign more!'}
101+
</Button>
102+
) : (
103+
!pending && (
104+
<Button
105+
size="sm"
106+
type="button"
107+
onClick={onSubmit}
108+
disabled={!signature}
109+
>
110+
Broadcast
111+
</Button>
112+
)
113+
)}
114+
</div>
115+
</div>
116+
);
117+
};
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use client';
2+
3+
import { Sign } from './sign';
4+
import { Broadcast } from './broadcast';
5+
import { useEffect, useState } from 'react';
6+
import { useSearchParams } from 'next/navigation';
7+
import { useLoggingIn } from '@useelven/core';
8+
import { Spinner } from '@/components/ui/spinner';
9+
10+
export const InscriptionsCreate = () => {
11+
const { pending } = useLoggingIn();
12+
const searchParams = useSearchParams();
13+
const [nextStep, setNextStep] = useState<boolean>();
14+
15+
// Web wallet handling
16+
useEffect(() => {
17+
const walletProviderStatus = searchParams.get('walletProviderStatus');
18+
if (walletProviderStatus === 'transactionsSigned') {
19+
setNextStep(true);
20+
}
21+
// eslint-disable-next-line react-hooks/exhaustive-deps
22+
}, []);
23+
24+
if (pending) {
25+
return (
26+
<div className="font-bold flex items-center gap-3 sm:px-8">
27+
<Spinner size={20} /> Pending, please wait...
28+
</div>
29+
);
30+
}
31+
32+
return (
33+
<div>
34+
{nextStep ? (
35+
<Broadcast setNextStep={setNextStep} />
36+
) : (
37+
<Sign setNextStep={setNextStep} />
38+
)}
39+
</div>
40+
);
41+
};
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
'use client';
2+
3+
import { usePersistStorage } from '@/hooks/use-form-storage';
4+
import sanitizeHtml from 'sanitize-html';
5+
import { Sha256 } from '@aws-crypto/sha256-browser';
6+
import { zodResolver } from '@hookform/resolvers/zod';
7+
import { useAccount, useLoginInfo, useSignMessage } from '@useelven/core';
8+
import { useForm } from 'react-hook-form';
9+
import { z } from 'zod';
10+
import { Form } from '@/components/ui/form';
11+
import { OperationsInputField } from '@/components/operations/operations-input-field';
12+
import { OperationsSubmitButton } from '@/components/operations/operations-submit-button';
13+
import { useEffect } from 'react';
14+
import { Spinner } from '@/components/ui/spinner';
15+
16+
const formSchema = z.object({
17+
inscription: z.string(),
18+
});
19+
20+
export const Sign = ({
21+
setNextStep,
22+
}: {
23+
setNextStep: (state: boolean) => void;
24+
}) => {
25+
const { address } = useAccount();
26+
const { loginMethod } = useLoginInfo();
27+
28+
const form = useForm<z.infer<typeof formSchema>>({
29+
resolver: zodResolver(formSchema),
30+
defaultValues: {
31+
inscription: '',
32+
},
33+
});
34+
35+
const { signMessage, signature, pending } = useSignMessage();
36+
37+
const { setItem: saveInscription } = usePersistStorage({
38+
update: (inscription) => {
39+
form.setValue('inscription', inscription);
40+
},
41+
storageItem: 'general-createInscription-inscription',
42+
withCleanup: false,
43+
});
44+
45+
const { setItem: savePayload } = usePersistStorage({
46+
storageItem: 'general-createInscription-partialPayload',
47+
withCleanup: false,
48+
});
49+
50+
const { setItem: saveSignature } = usePersistStorage({
51+
storageItem: 'general-createInscription-signature',
52+
withCleanup: false,
53+
});
54+
55+
const prepareData = async ({ inscription }: z.infer<typeof formSchema>) => {
56+
saveInscription(inscription);
57+
const sanitized = sanitizeHtml(
58+
inscription.replaceAll('\n', '').trim() || ''
59+
);
60+
61+
try {
62+
JSON.parse(sanitized);
63+
} catch {
64+
form.setError('inscription', {
65+
message:
66+
"You've provided the wrong JSON format. Could you try again? Beside the structure remember about double quotes (also for keys) and no trailing coma.",
67+
});
68+
return;
69+
}
70+
71+
const sanitizedBase64 = Buffer.from(sanitized).toString('base64');
72+
const hash = new Sha256();
73+
hash.update(sanitizedBase64);
74+
const shaValue = await hash.digest();
75+
const shaString = Buffer.from(shaValue.buffer).toString('hex');
76+
77+
await signMessage({
78+
message: shaString,
79+
options: { callbackUrl: '/inscriptions/create' },
80+
});
81+
82+
const payload = {
83+
identifier: shaString,
84+
data: sanitizedBase64,
85+
owner: address,
86+
};
87+
88+
savePayload(payload);
89+
};
90+
91+
useEffect(() => {
92+
if (signature) {
93+
saveSignature(signature);
94+
setNextStep(true);
95+
}
96+
// eslint-disable-next-line react-hooks/exhaustive-deps
97+
}, [signature]);
98+
99+
const getSigningProviderName = () => {
100+
if (loginMethod === 'walletconnect') {
101+
return 'xPortal';
102+
}
103+
return loginMethod;
104+
};
105+
106+
return (
107+
<>
108+
<div className="mb-3 sm:px-8">
109+
{pending && (
110+
<div className="font-bold flex items-center gap-3">
111+
<Spinner size={20} /> Transaction pending (confirmation through{' '}
112+
{getSigningProviderName()})...
113+
</div>
114+
)}
115+
</div>
116+
<div className="overflow-y-auto py-0 px-0 sm:px-8">
117+
<Form {...form}>
118+
<form
119+
id="inscription-form"
120+
onSubmit={form.handleSubmit(prepareData)}
121+
className="space-y-8"
122+
>
123+
<div className="flex-1 overflow-auto p-1">
124+
<OperationsInputField
125+
name="inscription"
126+
label="Inscription data"
127+
type="textarea"
128+
rows={10}
129+
placeholder='Example: { "myKey1": "myValue1", "myKey2": "myValue2" }'
130+
description="You can paste JSON data that will be then encoded with base64. You will be signing a sha256 hash of your data."
131+
/>
132+
</div>
133+
</form>
134+
</Form>
135+
</div>
136+
<div className="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 py-4 px-8">
137+
<OperationsSubmitButton
138+
formId="inscription-form"
139+
label="Sign the data first!"
140+
disabled={Boolean(signature)}
141+
pending={pending}
142+
/>
143+
</div>
144+
</>
145+
);
146+
};

app/inscriptions/create/page.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { NextPage } from 'next';
2+
import { InscriptionsCreate } from '../components/inscription-create';
3+
4+
const InscriptionsCreatePage: NextPage = () => {
5+
return <InscriptionsCreate />;
6+
};
7+
8+
export default InscriptionsCreatePage;

app/inscriptions/layout.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Metadata } from 'next';
2+
3+
const dappHostname = process.env.NEXT_PUBLIC_DAPP_HOST;
4+
const title = 'Inscriptions on MultiversX | Buildo.dev';
5+
const description =
6+
'Experimental Inscriptions. Save custom immutable data cheaper. You can then use it off-chain or for NFTs. (The structure of the data may change!';
7+
const globalImage = `${dappHostname}/og-image.png`;
8+
9+
export const metadata: Metadata = {
10+
title,
11+
description,
12+
openGraph: {
13+
title,
14+
images: [globalImage],
15+
description,
16+
type: 'website',
17+
url: '/inscriptions/create',
18+
},
19+
twitter: {
20+
title,
21+
description,
22+
images: [globalImage],
23+
card: 'summary_large_image',
24+
},
25+
};
26+
27+
export default function InscriptionsLayout({
28+
children,
29+
}: {
30+
children: React.ReactNode;
31+
}) {
32+
return (
33+
<div className="flex flex-col space-y-1.5 text-center sm:text-left pt-8 sm:p-8 pb-0">
34+
<div className="px-0 sm:px-8 mb-3">
35+
<h1 className="mb-3 text-lg font-semibold leading-none tracking-tight">
36+
Inscriptions
37+
</h1>
38+
<div className="text-sm text-muted-foreground">
39+
Experimental Inscriptions. Save custom immutable data cheaper. You can
40+
then use it off-chain or for NFTs. (The structure of the data may
41+
change!).
42+
<br /> Read more{' '}
43+
<a
44+
href="https://agora.multiversx.com/t/a-guide-for-builders-on-how-to-properly-create-and-manage-inscriptions-on-multiversx/303"
45+
target="_blank"
46+
className="underline"
47+
>
48+
here
49+
</a>{' '}
50+
and{' '}
51+
<a
52+
href="https://agora.multiversx.com/t/the-birth-of-inscriptionnfts/306"
53+
target="_blank"
54+
className="underline"
55+
>
56+
here
57+
</a>
58+
!
59+
</div>
60+
</div>
61+
<div>{children}</div>
62+
</div>
63+
);
64+
}

app/inscriptions/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { redirect } from 'next/navigation';
2+
3+
export default async function Inscriptions() {
4+
redirect('/inscriptions/create');
5+
}

0 commit comments

Comments
 (0)