Skip to content

Commit

Permalink
add experiment and fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
juliancwirko committed Dec 24, 2023
1 parent cd95502 commit c3c19d3
Show file tree
Hide file tree
Showing 18 changed files with 2,283 additions and 397 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
### [0.11.0](https://github.com/xdevguild/buildo.dev/releases/tag/v0.11.0) (2023-12-25)
- 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)
- fix sign message when using with redirections (web wallet)
- fix sign message using Ledger
- add possibility to open the operation dialog with URL link
- update dependencies

### [0.10.0](https://github.com/xdevguild/buildo.dev/releases/tag/v0.10.0) (2023-12-01)
- add sign a message operation

Expand Down
117 changes: 117 additions & 0 deletions app/inscriptions/components/broadcast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
'use client';

import { Button } from '@/components/ui/button';
import { Spinner } from '@/components/ui/spinner';
import { usePersistStorage } from '@/hooks/use-form-storage';
import { TransactionPayload } from '@multiversx/sdk-core/out';
import {
useAccount,
useConfig,
useLoginInfo,
useTransaction,
} from '@useelven/core';

export const Broadcast = ({
setNextStep,
}: {
setNextStep: (state: boolean) => void;
}) => {
const { address } = useAccount();
const { loginMethod } = useLoginInfo();
const { explorerAddress } = useConfig();
const { triggerTx, pending, txResult, error } = useTransaction({
webWalletRedirectUrl: '/inscriptions/create',
});

const { storageValue: inscription } = usePersistStorage({
storageItem: 'general-createInscription-inscription',
});

const { storageValue: rawPayload } = usePersistStorage({
storageItem: 'general-createInscription-partialPayload',
});

const { storageValue: signature } = usePersistStorage({
storageItem: 'general-createInscription-signature',
});

const onSubmit = async () => {
if (rawPayload && signature) {
const payload = JSON.stringify({
...rawPayload,
signature,
});

const data = new TransactionPayload(payload);

triggerTx?.({
address,
gasLimit: 50000 + 1500 * data.length(),
data,
value: 0,
});
}
};

const getSigningProviderName = () => {
if (loginMethod === 'walletconnect') {
return 'xPortal';
}
return loginMethod;
};

return (
<div className="px-0 sm:px-8">
<div className="mb-3 font-bold">
{txResult?.isCompleted && (
<div>
Your inscription was broadcasted. You can now find it{' '}
<a
href={`${explorerAddress}/transactions/${txResult.hash}`}
target="_blank"
className="underline"
>
on-chain
</a>
.
</div>
)}
{!txResult?.isCompleted && signature && (
<div>Your inscription was signed. Now You can broadcast it!</div>
)}
{error && <div>There was an error: {error}</div>}
</div>
<div className="mb-3">
{pending && (
<div className="font-bold flex items-center gap-3">
<Spinner size={20} /> Transaction pending (confirmation through{' '}
{getSigningProviderName()})...
</div>
)}
{!pending && inscription && (
<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">
{inscription}
</code>
)}
</div>
<div className="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 py-4 px-8">
{txResult?.isCompleted || error ? (
<Button size="sm" onClick={() => setNextStep(false)}>
{error ? 'Try again' : 'Sign more!'}
</Button>
) : (
!pending && (
<Button
size="sm"
type="button"
onClick={onSubmit}
disabled={!signature}
>
Broadcast
</Button>
)
)}
</div>
</div>
);
};
41 changes: 41 additions & 0 deletions app/inscriptions/components/inscription-create.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client';

import { Sign } from './sign';
import { Broadcast } from './broadcast';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { useLoggingIn } from '@useelven/core';
import { Spinner } from '@/components/ui/spinner';

export const InscriptionsCreate = () => {
const { pending } = useLoggingIn();
const searchParams = useSearchParams();
const [nextStep, setNextStep] = useState<boolean>();

// Web wallet handling
useEffect(() => {
const walletProviderStatus = searchParams.get('walletProviderStatus');
if (walletProviderStatus === 'transactionsSigned') {
setNextStep(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

if (pending) {
return (
<div className="font-bold flex items-center gap-3 sm:px-8">
<Spinner size={20} /> Pending, please wait...
</div>
);
}

return (
<div>
{nextStep ? (
<Broadcast setNextStep={setNextStep} />
) : (
<Sign setNextStep={setNextStep} />
)}
</div>
);
};
146 changes: 146 additions & 0 deletions app/inscriptions/components/sign.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
'use client';

import { usePersistStorage } from '@/hooks/use-form-storage';
import sanitizeHtml from 'sanitize-html';
import { Sha256 } from '@aws-crypto/sha256-browser';
import { zodResolver } from '@hookform/resolvers/zod';
import { useAccount, useLoginInfo, useSignMessage } from '@useelven/core';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Form } from '@/components/ui/form';
import { OperationsInputField } from '@/components/operations/operations-input-field';
import { OperationsSubmitButton } from '@/components/operations/operations-submit-button';
import { useEffect } from 'react';
import { Spinner } from '@/components/ui/spinner';

const formSchema = z.object({
inscription: z.string(),
});

export const Sign = ({
setNextStep,
}: {
setNextStep: (state: boolean) => void;
}) => {
const { address } = useAccount();
const { loginMethod } = useLoginInfo();

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
inscription: '',
},
});

const { signMessage, signature, pending } = useSignMessage();

const { setItem: saveInscription } = usePersistStorage({
update: (inscription) => {
form.setValue('inscription', inscription);
},
storageItem: 'general-createInscription-inscription',
withCleanup: false,
});

const { setItem: savePayload } = usePersistStorage({
storageItem: 'general-createInscription-partialPayload',
withCleanup: false,
});

const { setItem: saveSignature } = usePersistStorage({
storageItem: 'general-createInscription-signature',
withCleanup: false,
});

const prepareData = async ({ inscription }: z.infer<typeof formSchema>) => {
saveInscription(inscription);
const sanitized = sanitizeHtml(
inscription.replaceAll('\n', '').trim() || ''
);

try {
JSON.parse(sanitized);
} catch {
form.setError('inscription', {
message:
"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.",
});
return;
}

const sanitizedBase64 = Buffer.from(sanitized).toString('base64');
const hash = new Sha256();
hash.update(sanitizedBase64);
const shaValue = await hash.digest();
const shaString = Buffer.from(shaValue.buffer).toString('hex');

await signMessage({
message: shaString,
options: { callbackUrl: '/inscriptions/create' },
});

const payload = {
identifier: shaString,
data: sanitizedBase64,
owner: address,
};

savePayload(payload);
};

useEffect(() => {
if (signature) {
saveSignature(signature);
setNextStep(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [signature]);

const getSigningProviderName = () => {
if (loginMethod === 'walletconnect') {
return 'xPortal';
}
return loginMethod;
};

return (
<>
<div className="mb-3 sm:px-8">
{pending && (
<div className="font-bold flex items-center gap-3">
<Spinner size={20} /> Transaction pending (confirmation through{' '}
{getSigningProviderName()})...
</div>
)}
</div>
<div className="overflow-y-auto py-0 px-0 sm:px-8">
<Form {...form}>
<form
id="inscription-form"
onSubmit={form.handleSubmit(prepareData)}
className="space-y-8"
>
<div className="flex-1 overflow-auto p-1">
<OperationsInputField
name="inscription"
label="Inscription data"
type="textarea"
rows={10}
placeholder='Example: { "myKey1": "myValue1", "myKey2": "myValue2" }'
description="You can paste JSON data that will be then encoded with base64. You will be signing a sha256 hash of your data."
/>
</div>
</form>
</Form>
</div>
<div className="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 py-4 px-8">
<OperationsSubmitButton
formId="inscription-form"
label="Sign the data first!"
disabled={Boolean(signature)}
pending={pending}
/>
</div>
</>
);
};
8 changes: 8 additions & 0 deletions app/inscriptions/create/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { NextPage } from 'next';
import { InscriptionsCreate } from '../components/inscription-create';

const InscriptionsCreatePage: NextPage = () => {
return <InscriptionsCreate />;
};

export default InscriptionsCreatePage;
64 changes: 64 additions & 0 deletions app/inscriptions/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Metadata } from 'next';

const dappHostname = process.env.NEXT_PUBLIC_DAPP_HOST;
const title = 'Inscriptions on MultiversX | Buildo.dev';
const description =
'Experimental Inscriptions. Save custom immutable data cheaper. You can then use it off-chain or for NFTs. (The structure of the data may change!';
const globalImage = `${dappHostname}/og-image.png`;

export const metadata: Metadata = {
title,
description,
openGraph: {
title,
images: [globalImage],
description,
type: 'website',
url: '/inscriptions/create',
},
twitter: {
title,
description,
images: [globalImage],
card: 'summary_large_image',
},
};

export default function InscriptionsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex flex-col space-y-1.5 text-center sm:text-left pt-8 sm:p-8 pb-0">
<div className="px-0 sm:px-8 mb-3">
<h1 className="mb-3 text-lg font-semibold leading-none tracking-tight">
Inscriptions
</h1>
<div className="text-sm text-muted-foreground">
Experimental Inscriptions. Save custom immutable data cheaper. You can
then use it off-chain or for NFTs. (The structure of the data may
change!).
<br /> Read more{' '}
<a
href="https://agora.multiversx.com/t/a-guide-for-builders-on-how-to-properly-create-and-manage-inscriptions-on-multiversx/303"
target="_blank"
className="underline"
>
here
</a>{' '}
and{' '}
<a
href="https://agora.multiversx.com/t/the-birth-of-inscriptionnfts/306"
target="_blank"
className="underline"
>
here
</a>
!
</div>
</div>
<div>{children}</div>
</div>
);
}
5 changes: 5 additions & 0 deletions app/inscriptions/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { redirect } from 'next/navigation';

export default async function Inscriptions() {
redirect('/inscriptions/create');
}
Loading

1 comment on commit c3c19d3

@vercel
Copy link

@vercel vercel bot commented on c3c19d3 Dec 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.