From b18bdefcc0f73121acd5c13350cbac595fc67f44 Mon Sep 17 00:00:00 2001 From: thesn10 <38666407+thesn10@users.noreply.github.com> Date: Thu, 25 Jun 2026 19:43:22 +0200 Subject: [PATCH] feat: add canonical currency, alt currencies and accepted resolvers to create listing form --- app/src/components/CreateListingForm.tsx | 416 +++++++++++++++++++---- 1 file changed, 352 insertions(+), 64 deletions(-) diff --git a/app/src/components/CreateListingForm.tsx b/app/src/components/CreateListingForm.tsx index 1873d61..88f7993 100644 --- a/app/src/components/CreateListingForm.tsx +++ b/app/src/components/CreateListingForm.tsx @@ -7,34 +7,112 @@ import { isSome } from '@solana/kit' import { useTx } from '@/hooks/useTx' import { useListing } from '@/hooks/useListing' import { Button } from '@/components/ui/Button' -import { useToast } from '@/components/ui/Toast' import { getCreateListingInstructionAsync, getUpdateListingInstruction, } from '@solisting/sdk' -import type { Address } from '@solisting/sdk' +import type { Address, SolistingStateAltCurrencyConfigArgs } from '@solisting/sdk' + +interface AltEntry { + currency: string + oracle: string + decimals: string +} interface FormState { + canonicalKind: 'Sol' | 'Spl' + mint: string + decimals: string price: string quantity: string oracle: string metadataUri: string + alts: AltEntry[] + resolvers: string[] } function blank(): FormState { - return { price: '', quantity: '', oracle: '', metadataUri: '' } + return { + canonicalKind: 'Sol', + mint: '', + decimals: '6', + price: '', + quantity: '', + oracle: '', + metadataUri: '', + alts: [], + resolvers: [], + } } interface Props { editPk?: Address } +const LABEL: React.CSSProperties = { + display: 'block', + fontSize: 12, + fontWeight: 600, + color: 'var(--mut)', + marginBottom: 8, + letterSpacing: '.04em', +} +const INPUT: React.CSSProperties = { + width: '100%', + height: 42, + padding: '0 13px', + background: 'var(--bg3)', + border: '1px solid var(--bd)', + borderRadius: 10, + fontSize: 13, + outline: 'none', + color: 'inherit', + boxSizing: 'border-box', +} +const INPUT_MONO: React.CSSProperties = { ...INPUT, fontFamily: 'var(--font-mono)' } +const SMALL_INPUT: React.CSSProperties = { + ...INPUT, + height: 40, + borderRadius: 9, + fontSize: 12.5, +} +const SMALL_MONO: React.CSSProperties = { ...SMALL_INPUT, fontFamily: 'var(--font-mono)' } + +function CurrencyToggle({ + value, + onChange, +}: { + value: 'Sol' | 'Spl' + onChange: (v: 'Sol' | 'Spl') => void +}) { + const active = (on: boolean): React.CSSProperties => ({ + flex: 1, + padding: 11, + borderRadius: 10, + fontSize: 13, + fontWeight: 600, + cursor: 'pointer', + color: on ? '#0b0613' : 'var(--mut)', + background: on ? 'linear-gradient(135deg,var(--acc),var(--acc2))' : 'var(--bg3)', + border: `1px solid ${on ? 'transparent' : 'var(--bd)'}`, + }) + return ( +
+ + +
+ ) +} + export function CreateListingForm({ editPk }: Props) { const router = useRouter() const { signer } = useKitTransactionSigner() const { account } = useWallet() const sendTx = useTx() - const toast = useToast() const { data: existing } = useListing(editPk ?? ('' as Address)) const [form, setForm] = useState(blank()) @@ -44,22 +122,119 @@ export function CreateListingForm({ editPk }: Props) { const isUpdate = !!editPk + // Pre-fill from on-chain data on first load if (!initialized && existing && isUpdate) { setInitialized(true) const d = existing.data + const cur = d.canonicalCurrency + const isSpl = cur.__kind === 'Spl' setForm({ - price: (Number(d.price) / 1e9).toString(), + canonicalKind: cur.__kind, + mint: isSpl ? (cur as { __kind: 'Spl'; mint: Address; decimals: number }).mint : '', + decimals: isSpl + ? String((cur as { __kind: 'Spl'; mint: Address; decimals: number }).decimals) + : '6', + price: isSpl + ? String( + Number(d.price) / + Math.pow( + 10, + (cur as { __kind: 'Spl'; mint: Address; decimals: number }).decimals, + ), + ) + : (Number(d.price) / 1e9).toString(), quantity: String(d.quantity), oracle: isSome(d.canonicalOracle) ? d.canonicalOracle.value : '', metadataUri: d.metadataUri ?? '', + alts: d.altCurrencies.map((a) => ({ + currency: + a.currency.__kind === 'Sol' + ? 'SOL' + : (a.currency as { __kind: 'Spl'; mint: Address; decimals: number }).mint, + oracle: isSome(a.usdOracle) ? a.usdOracle.value : '', + decimals: + a.currency.__kind === 'Spl' + ? String( + (a.currency as { __kind: 'Spl'; mint: Address; decimals: number }).decimals, + ) + : '6', + })), + resolvers: [...d.acceptedResolvers], }) } - function set(key: keyof FormState) { + function set(key: K) { return (e: React.ChangeEvent) => setForm((f) => ({ ...f, [key]: e.target.value })) } + // Alt currency helpers + function addAlt() { + if (form.alts.length >= 3) return + setForm((f) => ({ ...f, alts: [...f.alts, { currency: '', oracle: '', decimals: '6' }] })) + } + function removeAlt(i: number) { + setForm((f) => ({ ...f, alts: f.alts.filter((_, idx) => idx !== i) })) + } + function setAlt(i: number, key: keyof AltEntry, val: string) { + setForm((f) => { + const alts = f.alts.map((a, idx) => (idx === i ? { ...a, [key]: val } : a)) + return { ...f, alts } + }) + } + + // Resolver helpers + function addResolver() { + if (form.resolvers.length >= 4) return + setForm((f) => ({ ...f, resolvers: [...f.resolvers, ''] })) + } + function removeResolver(i: number) { + setForm((f) => ({ ...f, resolvers: f.resolvers.filter((_, idx) => idx !== i) })) + } + function setResolver(i: number, val: string) { + setForm((f) => { + const resolvers = f.resolvers.map((r, idx) => (idx === i ? val : r)) + return { ...f, resolvers } + }) + } + + function buildCurrency() { + if (form.canonicalKind === 'Sol') return { __kind: 'Sol' as const } + const dec = parseInt(form.decimals, 10) + return { __kind: 'Spl' as const, mint: form.mint as Address, decimals: isNaN(dec) ? 6 : dec } + } + + function buildPrice() { + const n = parseFloat(form.price) + if (isNaN(n) || n <= 0) throw new Error('Invalid price') + if (form.canonicalKind === 'Sol') return BigInt(Math.round(n * 1e9)) + const dec = parseInt(form.decimals, 10) + return BigInt(Math.round(n * Math.pow(10, isNaN(dec) ? 6 : dec))) + } + + function buildAlts(): SolistingStateAltCurrencyConfigArgs[] { + return form.alts + .filter((a) => a.currency.trim()) + .map((a) => { + const isSol = a.currency.trim().toUpperCase() === 'SOL' + const dec = parseInt(a.decimals, 10) + return { + currency: isSol + ? { __kind: 'Sol' as const } + : { + __kind: 'Spl' as const, + mint: a.currency.trim() as Address, + decimals: isNaN(dec) ? 6 : dec, + }, + usdOracle: a.oracle.trim() ? (a.oracle.trim() as Address) : null, + } + }) + } + + function buildResolvers(): Address[] { + return form.resolvers.filter((r) => r.trim().length > 30).map((r) => r.trim() as Address) + } + async function submit(e: React.FormEvent) { e.preventDefault() if (!signer || !account) { @@ -69,41 +244,36 @@ export function CreateListingForm({ editPk }: Props) { setError(null) setLoading(true) try { - const priceN = parseFloat(form.price) - if (isNaN(priceN) || priceN <= 0) throw new Error('Invalid price') - const priceLamports = BigInt(Math.round(priceN * 1e9)) + const price = buildPrice() const quantityN = parseInt(form.quantity, 10) if (isNaN(quantityN) || quantityN <= 0) throw new Error('Invalid quantity') - const canonicalOracle = form.oracle ? (form.oracle as Address) : null + const canonicalOracle = form.oracle.trim() ? (form.oracle.trim() as Address) : null + const canonicalCurrency = buildCurrency() + const altCurrencies = buildAlts() + const acceptedResolvers = buildResolvers() if (isUpdate && editPk) { - const existingData = existing?.data const ix = getUpdateListingInstruction({ seller: signer, listingAccount: editPk, - canonicalCurrency: existingData?.canonicalCurrency ?? { __kind: 'Sol' }, - price: priceLamports, + canonicalCurrency, + price, canonicalOracle, - altCurrencies: existingData?.altCurrencies ?? [], - acceptedResolvers: existingData?.acceptedResolvers ?? [], + altCurrencies, + acceptedResolvers, quantity: quantityN, metadataUri: form.metadataUri, }) - await sendTx( - [ix as never], - [['listings'], ['listing', editPk]], - 'update_listing', - ) + await sendTx([ix as never], [['listings'], ['listing', editPk]], 'update_listing') } else { - const listingId = BigInt(Date.now()) const ix = await getCreateListingInstructionAsync({ seller: signer, - listingId, - canonicalCurrency: { __kind: 'Sol' }, - price: priceLamports, + listingId: BigInt(Date.now()), + canonicalCurrency, + price, canonicalOracle, - altCurrencies: [], - acceptedResolvers: [], + altCurrencies, + acceptedResolvers, quantity: quantityN, metadataUri: form.metadataUri, }) @@ -117,26 +287,12 @@ export function CreateListingForm({ editPk }: Props) { } } - const LABEL_STYLE = { - display: 'block', - fontSize: 12, - fontWeight: 600, - color: 'var(--mut)', - marginBottom: 8, - letterSpacing: '.04em', - } as const - const INPUT_STYLE = { - width: '100%', - height: 42, - padding: '0 13px', - background: 'var(--bg3)', - border: '1px solid var(--bd)', - borderRadius: 10, - fontSize: 13, - outline: 'none', - color: 'inherit', - boxSizing: 'border-box' as const, - } as const + const priceUnit = + form.canonicalKind === 'Sol' + ? 'SOL' + : form.mint + ? form.mint.slice(0, 6) + '…' + : 'SPL' return (
@@ -160,10 +316,10 @@ export function CreateListingForm({ editPk }: Props) {

{isUpdate ? 'Update Listing' : 'Create Listing'}

-

+

{isUpdate ? `Editing listing ${editPk?.slice(0, 8)}…` - : 'New SOL-priced listing on the solisting program'} + : 'New listing on the solisting program'}

+ {/* CANONICAL CURRENCY */} +
+ + setForm((f) => ({ ...f, canonicalKind: v }))} + /> + {form.canonicalKind === 'Spl' && ( +
+ + +
+ )} +
+ + {/* PRICE + QUANTITY */}
- - + +
- - + +
+ {/* CANONICAL ORACLE */}
-
+ {/* ALT CURRENCIES */}
-
+ + {/* ACCEPTED RESOLVERS */} +
+
+ + {form.resolvers.length < 4 && ( + + )} +
+ {form.resolvers.map((rv, i) => ( +
+ setResolver(i, e.target.value)} + placeholder="Resolver pubkey" + style={{ ...SMALL_MONO, flex: 1 }} + /> + +
+ ))} +
+ + {/* METADATA URI */} +
+ @@ -211,7 +506,7 @@ export function CreateListingForm({ editPk }: Props) { value={form.metadataUri} onChange={set('metadataUri')} placeholder="ipfs://… or https://…" - style={INPUT_STYLE} + style={INPUT_MONO} />
@@ -229,14 +524,7 @@ export function CreateListingForm({ editPk }: Props) {
)} -
+