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'}
)}
-
+