-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
cd95502
commit c3c19d3
Showing
18 changed files
with
2,283 additions
and
397 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
</> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} |
Oops, something went wrong.
c3c19d3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
buildo-dev – ./
buildo-dev-elven-family-6f0851cc.vercel.app
buildo-dev-git-main-elven-family-6f0851cc.vercel.app
www.buildo.dev
buildo-dev.vercel.app
buildo.dev