Compare commits
11 Commits
b18bdefcc0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3097fdc8a | ||
|
|
aafbc17e94 | ||
|
|
aac025e2a8 | ||
|
|
e799c0be9d | ||
|
|
0c86751d51 | ||
|
|
02c26e186f | ||
|
|
12f1344138 | ||
|
|
94d2d67e83 | ||
|
|
ec1c7d9f8c | ||
|
|
555598535d | ||
|
|
ca1c0b1cd8 |
@@ -18,3 +18,11 @@ wallet = "~/.config/solana/id.json"
|
|||||||
test = "cargo test"
|
test = "cargo test"
|
||||||
|
|
||||||
[hooks]
|
[hooks]
|
||||||
|
|
||||||
|
[[test.genesis]]
|
||||||
|
address = "DjVR4EuYV6USMJFfsGZwhZ3y8rtWsmG8EvDY96GTqqi3"
|
||||||
|
program = "../descro/target/deploy/descro.so"
|
||||||
|
|
||||||
|
[[test.genesis]]
|
||||||
|
address = "GwUPAKs3HHzCpj8uhet4NAnxk9GWNwfrYbpihu5DyFp"
|
||||||
|
program = "../descro/target/deploy/descro_ext_resolvers.so"
|
||||||
|
|||||||
120
README.md
Normal file
120
README.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# solisting
|
||||||
|
|
||||||
|
## Vision
|
||||||
|
|
||||||
|
Today's marketplaces are gatekept: a company can delist a product, freeze a seller's payout, or shut down the platform outright, and buyers/sellers have no recourse. `solisting` is built on the belief that product listings and the payments behind them should live on-chain instead — where no single operator can censor a listing, and no middleman sits between buyer and seller taking a cut or holding custody.
|
||||||
|
|
||||||
|
- 🌍 **Decentralized marketplaces** — listings and orders live as on-chain accounts, not rows in a company's database, so there's no central operator who can arbitrarily delist products or gatekeep who gets to sell.
|
||||||
|
- 🚫 **No middleman** — buyers and sellers interact directly through the protocol; there's no platform sitting between them extracting fees, controlling access, or acting as a single point of failure.
|
||||||
|
- 🧱 **Censorship-resistant listings** — as long as the underlying blockchain is live, a listing exists and stays visible; it can't be quietly taken down by a platform decision.
|
||||||
|
- ✅ **Final, auditable payments** — every payment flows through smart contract escrow, so settlement is final and every step of it is publicly verifiable on-chain, not hidden inside a company's internal ledger.
|
||||||
|
- 🔮 **Predictable by construction** — the rules for how funds move (when they're locked, when they're released, when they're refunded) are enforced by code, not discretionary platform policy, so outcomes are known in advance rather than decided case-by-case.
|
||||||
|
|
||||||
|
## What is solisting?
|
||||||
|
|
||||||
|
`solisting` is a single-program Solana protocol that implements a **coordination and discovery layer for decentralized product listings and bilateral order consent**. It sits on top of a separate escrow program, [`descro`](../descro), and never custodies funds itself — it CPIs into `descro` to create, confirm, and cancel escrows.
|
||||||
|
Built using [Anchor](https://www.anchor-lang.com/).
|
||||||
|
|
||||||
|
The repository also contains a TypeScript SDK and a Next.js explorer app built on top of the on-chain program.
|
||||||
|
|
||||||
|
## Concept
|
||||||
|
|
||||||
|
Marketplaces on Solana usually force a choice: either the marketplace program custodies funds itself (more trust assumptions, more surface area to audit), or every integration reinvents escrow from scratch. `solisting` takes a different approach — it is a thin coordination layer that only handles *discovery* (listings) and *consent* (orders), and delegates all fund custody to a dedicated, independently auditable escrow program (`descro`) via CPI.
|
||||||
|
|
||||||
|
- 🔐 **Non-custodial by design** — solisting never holds buyer or seller funds; every escrow lifecycle event is a CPI into `descro`, so the fund-custody logic lives in one focused, auditable program instead of being duplicated per marketplace.
|
||||||
|
- 🧩 **Composable, not monolithic** — separating listings/orders from escrow means `descro` can be reused by other marketplace front-ends, and `solisting` can evolve its discovery/consent UX independently of settlement logic.
|
||||||
|
- 💱 **Multi-currency listings from one source of truth** — a seller sets a single canonical price; buyers can pay in alternate currencies (including SOL or stablecoins) and the program converts on the fly via Pyth oracles, so there's no need to maintain N parallel prices per listing.
|
||||||
|
- 🛡️ **Slippage-protected conversion** — buyers supply an expected amount and max slippage tolerance, so oracle-based conversion can't silently overcharge them between quote and execution.
|
||||||
|
- 🤝 **Bilateral consent, defensively handled** — orders require explicit seller acceptance, and abort paths (`reject_order`/`cancel_order`) tolerate the counterparty already having cancelled directly on `descro`, avoiding stuck states.
|
||||||
|
|
||||||
|
## Repository layout
|
||||||
|
|
||||||
|
```
|
||||||
|
programs/solisting/ # Anchor program (Rust)
|
||||||
|
sdk/ # TypeScript client SDK (Codama-generated + hand-written helpers)
|
||||||
|
app/ # Next.js app for browsing listings/orders
|
||||||
|
docs/ # Implementation plans and design docs
|
||||||
|
vendor/ # Vendored/patched Pyth crates (see "Toolchain gotchas" below)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Canonical price model
|
||||||
|
|
||||||
|
`ListingAccount` stores one canonical price (`canonical_currency` + `price`) as the sole source of truth:
|
||||||
|
|
||||||
|
- `alt_currencies` — other currencies a buyer may pay in, each with an optional Pyth `TOKEN/USD` oracle (`None` = treated as a $1.00 stablecoin).
|
||||||
|
- `canonical_oracle` — the Pyth feed for the canonical currency (`None` = canonical is itself a $1 stablecoin).
|
||||||
|
|
||||||
|
At `create_order` time, if the buyer pays in an alt currency, the program converts the canonical price into the target currency using USD as a pivot through up to two Pyth feeds, and enforces a buyer-supplied slippage tolerance.
|
||||||
|
|
||||||
|
### Accounts & PDAs
|
||||||
|
|
||||||
|
- `ListingAccount` — seeds `[b"listing", seller, listing_id]`. Tracks quantity, reservations, accepted resolvers, active state, and metadata URI.
|
||||||
|
- `OrderAccount` — seeds `[b"order", listing_account, buyer, order_id]`. Records the buyer's chosen payment currency, the oracle-resolved amount, and a pointer to the corresponding `descro` `EscrowAccount`.
|
||||||
|
|
||||||
|
### Instructions
|
||||||
|
|
||||||
|
| Instruction | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `create_listing` / `update_listing` / `close_listing` | Manage a listing (seller-only) |
|
||||||
|
| `create_order` | Buyer flow: validates currency/resolver, performs oracle conversion, CPIs into `descro` to create an escrow |
|
||||||
|
| `accept_order` | Seller flow: confirms the escrow and settles the listing's reserved quantity |
|
||||||
|
| `reject_order` / `cancel_order` | Seller/buyer-initiated abort, defensive against out-of-band cancellation on `descro` |
|
||||||
|
| `close_stale_order` | Permissionless cleanup of orders whose escrow has already reached a terminal state |
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Rust toolchain `1.89.0` (pinned via `rust-toolchain.toml`)
|
||||||
|
- Solana CLI / `cargo build-sbf`
|
||||||
|
- The sibling [`descro`](../descro) repository checked out at `../descro` relative to this repo — solisting path-depends on it and the tests embed its compiled `.so` files.
|
||||||
|
- Node.js + Yarn (for the SDK and app)
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
`descro` (and its dependency `descro_ext_resolvers`) must be built first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Build dependency programs (sibling repo)
|
||||||
|
cd descro && anchor build
|
||||||
|
|
||||||
|
# 2. Build solisting
|
||||||
|
anchor build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Requires steps 1 + 2 above to have been run first, since the LiteSVM test harness embeds both `.so` files via `include_bytes!`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test --manifest-path programs/solisting/Cargo.toml
|
||||||
|
|
||||||
|
# Run one test file / one test by name
|
||||||
|
cargo test --manifest-path programs/solisting/Cargo.toml --test test_orders
|
||||||
|
cargo test --manifest-path programs/solisting/Cargo.toml -- seller_can_create_sol_only_listing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lint / format
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo clippy --manifest-path programs/solisting/Cargo.toml
|
||||||
|
cargo fmt --manifest-path programs/solisting/Cargo.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `Anchor.toml` declares `test = "cargo test"`, but `anchor test` is not the normal workflow here — use the `cargo` commands above directly.
|
||||||
|
|
||||||
|
## SDK & app
|
||||||
|
|
||||||
|
The `sdk/` and `app/` workspaces are managed with Yarn:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn install
|
||||||
|
yarn workspace @solisting/sdk ... # SDK-specific commands
|
||||||
|
```
|
||||||
|
|
||||||
|
See their respective `package.json` files and `docs/superpowers/` for details on the SDK and explorer app.
|
||||||
|
|
||||||
|
## Toolchain gotchas
|
||||||
|
|
||||||
|
- The Rust toolchain is pinned to `1.89.0` via `rust-toolchain.toml`.
|
||||||
|
- The workspace root `Cargo.toml` patches `pyth-solana-receiver-sdk` and `pythnet-sdk` to local copies under `vendor/` because the crates.io versions don't compile against this toolchain's dependency graph (an `anchor-lang`/`borsh` version conflict). If you touch oracle code, the vendored crates are the source of truth for `PriceUpdateV2`'s shape.
|
||||||
8
app/src/app/escrow/[pk]/page.tsx
Normal file
8
app/src/app/escrow/[pk]/page.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { use } from 'react'
|
||||||
|
import { EscrowDetail } from '@/components/EscrowDetail'
|
||||||
|
import type { Address } from '@solana/kit'
|
||||||
|
|
||||||
|
export default function EscrowPage({ params }: { params: Promise<{ pk: string }> }) {
|
||||||
|
const { pk } = use(params)
|
||||||
|
return <EscrowDetail pda={pk as Address} />
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { useWallet, useKitTransactionSigner } from '@solana/connector/react'
|
|||||||
import { ListingDetail } from '@/components/ListingDetail'
|
import { ListingDetail } from '@/components/ListingDetail'
|
||||||
import { useTx } from '@/hooks/useTx'
|
import { useTx } from '@/hooks/useTx'
|
||||||
import { useListing } from '@/hooks/useListing'
|
import { useListing } from '@/hooks/useListing'
|
||||||
|
import { useResolvers } from '@/hooks/useResolvers'
|
||||||
import {
|
import {
|
||||||
getCloseListingInstruction,
|
getCloseListingInstruction,
|
||||||
getCreateOrderInstructionAsync,
|
getCreateOrderInstructionAsync,
|
||||||
@@ -13,6 +14,8 @@ import {
|
|||||||
deriveEscrowId,
|
deriveEscrowId,
|
||||||
} from '@solisting/sdk'
|
} from '@solisting/sdk'
|
||||||
import { DESCRO_PROGRAM_ADDRESS } from '@descro/sdk'
|
import { DESCRO_PROGRAM_ADDRESS } from '@descro/sdk'
|
||||||
|
import { ResolverType } from '@descro/sdk'
|
||||||
|
import { Button } from '@/components/ui/Button'
|
||||||
import type { Address } from '@solisting/sdk'
|
import type { Address } from '@solisting/sdk'
|
||||||
|
|
||||||
const SYSTEM_PROGRAM = '11111111111111111111111111111111' as Address<'11111111111111111111111111111111'>
|
const SYSTEM_PROGRAM = '11111111111111111111111111111111' as Address<'11111111111111111111111111111111'>
|
||||||
@@ -24,7 +27,10 @@ export default function ListingPage({ params }: { params: Promise<{ pk: string }
|
|||||||
const { signer } = useKitTransactionSigner()
|
const { signer } = useKitTransactionSigner()
|
||||||
const sendTx = useTx()
|
const sendTx = useTx()
|
||||||
const { data: listing } = useListing(pk as Address)
|
const { data: listing } = useListing(pk as Address)
|
||||||
|
const { data: resolvers = [] } = useResolvers()
|
||||||
const [orderLoading, setOrderLoading] = useState(false)
|
const [orderLoading, setOrderLoading] = useState(false)
|
||||||
|
const [showResolverPicker, setShowResolverPicker] = useState(false)
|
||||||
|
const [selectedResolver, setSelectedResolver] = useState<Address | null>(null)
|
||||||
|
|
||||||
async function handleClose() {
|
async function handleClose() {
|
||||||
if (!signer) return
|
if (!signer) return
|
||||||
@@ -33,8 +39,14 @@ export default function ListingPage({ params }: { params: Promise<{ pk: string }
|
|||||||
router.push('/listings')
|
router.push('/listings')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePlaceOrder() {
|
function openPlaceOrder() {
|
||||||
|
setSelectedResolver(null)
|
||||||
|
setShowResolverPicker(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConfirmOrder() {
|
||||||
if (!signer || !account || !listing) return
|
if (!signer || !account || !listing) return
|
||||||
|
setShowResolverPicker(false)
|
||||||
setOrderLoading(true)
|
setOrderLoading(true)
|
||||||
try {
|
try {
|
||||||
const orderId = BigInt(Date.now())
|
const orderId = BigInt(Date.now())
|
||||||
@@ -50,7 +62,7 @@ export default function ListingPage({ params }: { params: Promise<{ pk: string }
|
|||||||
descroProgram: DESCRO_PROGRAM_ADDRESS,
|
descroProgram: DESCRO_PROGRAM_ADDRESS,
|
||||||
orderId,
|
orderId,
|
||||||
escrowId,
|
escrowId,
|
||||||
resolver: account as Address,
|
resolver: selectedResolver,
|
||||||
paymentCurrency: { __kind: 'Sol' },
|
paymentCurrency: { __kind: 'Sol' },
|
||||||
expectedAmount: listing.data.price,
|
expectedAmount: listing.data.price,
|
||||||
maxSlippageBps: 0,
|
maxSlippageBps: 0,
|
||||||
@@ -60,20 +72,108 @@ export default function ListingPage({ params }: { params: Promise<{ pk: string }
|
|||||||
[['listing', pk], ['orders', pk]],
|
[['listing', pk], ['orders', pk]],
|
||||||
'create_order',
|
'create_order',
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch {
|
||||||
console.error('create_order failed:', err)
|
// error already shown via toast in useTx
|
||||||
} finally {
|
} finally {
|
||||||
setOrderLoading(false)
|
setOrderLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const acceptedResolvers = listing?.data.acceptedResolvers ?? []
|
||||||
|
const visibleResolvers = acceptedResolvers.length > 0
|
||||||
|
? resolvers.filter((r) => acceptedResolvers.includes(r.pda as Address))
|
||||||
|
: resolvers
|
||||||
|
const mustSelectResolver = acceptedResolvers.length > 0
|
||||||
|
const confirmDisabled = mustSelectResolver && !selectedResolver
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListingDetail
|
<>
|
||||||
pk={pk as Address}
|
<ListingDetail
|
||||||
walletAddress={account ?? null}
|
pk={pk as Address}
|
||||||
onUpdate={() => router.push(`/listing/create?edit=${pk}`)}
|
walletAddress={account ?? null}
|
||||||
onClose={handleClose}
|
onUpdate={() => router.push(`/listing/create?edit=${pk}`)}
|
||||||
onPlaceOrder={orderLoading ? undefined : handlePlaceOrder}
|
onClose={handleClose}
|
||||||
/>
|
onPlaceOrder={orderLoading ? undefined : openPlaceOrder}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{showResolverPicker && (
|
||||||
|
<div
|
||||||
|
style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,.55)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 100 }}
|
||||||
|
onClick={(e) => { if (e.target === e.currentTarget) setShowResolverPicker(false) }}
|
||||||
|
>
|
||||||
|
<div style={{ background: 'var(--bg)', border: '1px solid var(--bd)', borderRadius: 'var(--radius)', padding: '28px 28px 24px', width: 440, maxWidth: '90vw', maxHeight: '80vh', overflow: 'auto' }}>
|
||||||
|
<div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '.14em', color: 'var(--mut)', marginBottom: 12 }}>PLACE ORDER</div>
|
||||||
|
<h2 style={{ margin: '0 0 8px', fontSize: 19, fontWeight: 700 }}>Select Resolver</h2>
|
||||||
|
<p style={{ margin: '0 0 20px', fontSize: 13, color: 'var(--mut)', lineHeight: 1.55 }}>
|
||||||
|
{mustSelectResolver
|
||||||
|
? 'This listing requires one of the following resolvers for dispute resolution.'
|
||||||
|
: 'Choose a resolver for dispute resolution, or proceed without one.'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 22 }}>
|
||||||
|
{!mustSelectResolver && (
|
||||||
|
<ResolverOption
|
||||||
|
selected={selectedResolver === null}
|
||||||
|
onClick={() => setSelectedResolver(null)}
|
||||||
|
label="No resolver"
|
||||||
|
sublabel="Disputes cannot be resolved on-chain"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{visibleResolvers.map((r) => (
|
||||||
|
<ResolverOption
|
||||||
|
key={r.pda}
|
||||||
|
selected={selectedResolver === r.pda}
|
||||||
|
onClick={() => setSelectedResolver(r.pda as Address)}
|
||||||
|
label={`${r.pda.slice(0, 8)}…${r.pda.slice(-6)}`}
|
||||||
|
sublabel={ResolverType[r.data.resolverType as number] as string}
|
||||||
|
mono
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{visibleResolvers.length === 0 && mustSelectResolver && (
|
||||||
|
<div style={{ fontSize: 13, color: 'var(--mut)', padding: '12px 0' }}>
|
||||||
|
No registered resolvers found for this listing.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end' }}>
|
||||||
|
<Button variant="ghost" onClick={() => setShowResolverPicker(false)}>Cancel</Button>
|
||||||
|
<Button onClick={handleConfirmOrder} disabled={confirmDisabled}>Place Order</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResolverOption({ selected, onClick, label, sublabel, mono }: {
|
||||||
|
selected: boolean
|
||||||
|
onClick: () => void
|
||||||
|
label: string
|
||||||
|
sublabel?: string
|
||||||
|
mono?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
style={{
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12,
|
||||||
|
padding: '11px 14px', borderRadius: 9, cursor: 'pointer', textAlign: 'left',
|
||||||
|
background: selected ? 'var(--accSoft)' : 'var(--bg3)',
|
||||||
|
border: `1px solid ${selected ? 'var(--accBd)' : 'var(--bd)'}`,
|
||||||
|
color: 'var(--tx)',
|
||||||
|
transition: 'background .12s, border-color .12s',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontFamily: mono ? 'var(--font-mono)' : undefined, fontSize: mono ? 12.5 : 13, fontWeight: selected ? 600 : mono ? 400 : 600 }}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
{sublabel && (
|
||||||
|
<span style={{ fontSize: 11.5, color: 'var(--mut)', flexShrink: 0 }}>
|
||||||
|
{sublabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,11 @@ import {
|
|||||||
DESCRO_PROGRAM_ADDRESS,
|
DESCRO_PROGRAM_ADDRESS,
|
||||||
findResolverEntryPda,
|
findResolverEntryPda,
|
||||||
} from '@descro/sdk'
|
} from '@descro/sdk'
|
||||||
|
import { isSome } from '@solana/kit'
|
||||||
import type { Address } from '@solisting/sdk'
|
import type { Address } from '@solisting/sdk'
|
||||||
|
|
||||||
|
const SYSTEM_PROGRAM = '11111111111111111111111111111111' as Address
|
||||||
|
|
||||||
export default function OrderPage({ params }: { params: Promise<{ pk: string }> }) {
|
export default function OrderPage({ params }: { params: Promise<{ pk: string }> }) {
|
||||||
const { pk } = use(params)
|
const { pk } = use(params)
|
||||||
const { account } = useWallet()
|
const { account } = useWallet()
|
||||||
@@ -31,10 +34,14 @@ export default function OrderPage({ params }: { params: Promise<{ pk: string }>
|
|||||||
async function handleAccept() {
|
async function handleAccept() {
|
||||||
if (!signer || !data) return
|
if (!signer || !data) return
|
||||||
const od = data.order.data
|
const od = data.order.data
|
||||||
const [resolverEntry] = await findResolverEntryPda({ authority: od.resolver })
|
const resolverAddr = isSome(od.resolver) ? od.resolver.value : null
|
||||||
|
const resolverKey = resolverAddr ?? SYSTEM_PROGRAM
|
||||||
|
const [resolverEntry] = resolverAddr
|
||||||
|
? await findResolverEntryPda({ authority: resolverAddr })
|
||||||
|
: [SYSTEM_PROGRAM]
|
||||||
const ix = getAcceptOrderInstruction({
|
const ix = getAcceptOrderInstruction({
|
||||||
seller: signer,
|
seller: signer,
|
||||||
resolver: od.resolver,
|
resolver: resolverKey,
|
||||||
listingAccount: od.listing,
|
listingAccount: od.listing,
|
||||||
orderAccount: pk as Address,
|
orderAccount: pk as Address,
|
||||||
escrowAccount: od.escrowAccount,
|
escrowAccount: od.escrowAccount,
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ interface FormState {
|
|||||||
price: string
|
price: string
|
||||||
quantity: string
|
quantity: string
|
||||||
oracle: string
|
oracle: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
metadataUri: string
|
metadataUri: string
|
||||||
alts: AltEntry[]
|
alts: AltEntry[]
|
||||||
resolvers: string[]
|
resolvers: string[]
|
||||||
@@ -39,6 +41,8 @@ function blank(): FormState {
|
|||||||
price: '',
|
price: '',
|
||||||
quantity: '',
|
quantity: '',
|
||||||
oracle: '',
|
oracle: '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
metadataUri: '',
|
metadataUri: '',
|
||||||
alts: [],
|
alts: [],
|
||||||
resolvers: [],
|
resolvers: [],
|
||||||
@@ -87,6 +91,9 @@ function CurrencyToggle({
|
|||||||
}) {
|
}) {
|
||||||
const active = (on: boolean): React.CSSProperties => ({
|
const active = (on: boolean): React.CSSProperties => ({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
padding: 11,
|
padding: 11,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
@@ -94,7 +101,7 @@ function CurrencyToggle({
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
color: on ? '#0b0613' : 'var(--mut)',
|
color: on ? '#0b0613' : 'var(--mut)',
|
||||||
background: on ? 'linear-gradient(135deg,var(--acc),var(--acc2))' : 'var(--bg3)',
|
background: on ? 'linear-gradient(135deg,var(--acc),var(--acc2))' : 'var(--bg3)',
|
||||||
border: `1px solid ${on ? 'transparent' : 'var(--bd)'}`,
|
border: `1px solid var(--bd)`,
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
@@ -145,6 +152,8 @@ export function CreateListingForm({ editPk }: Props) {
|
|||||||
: (Number(d.price) / 1e9).toString(),
|
: (Number(d.price) / 1e9).toString(),
|
||||||
quantity: String(d.quantity),
|
quantity: String(d.quantity),
|
||||||
oracle: isSome(d.canonicalOracle) ? d.canonicalOracle.value : '',
|
oracle: isSome(d.canonicalOracle) ? d.canonicalOracle.value : '',
|
||||||
|
name: d.name ?? '',
|
||||||
|
description: d.description ?? '',
|
||||||
metadataUri: d.metadataUri ?? '',
|
metadataUri: d.metadataUri ?? '',
|
||||||
alts: d.altCurrencies.map((a) => ({
|
alts: d.altCurrencies.map((a) => ({
|
||||||
currency:
|
currency:
|
||||||
@@ -262,6 +271,8 @@ export function CreateListingForm({ editPk }: Props) {
|
|||||||
altCurrencies,
|
altCurrencies,
|
||||||
acceptedResolvers,
|
acceptedResolvers,
|
||||||
quantity: quantityN,
|
quantity: quantityN,
|
||||||
|
name: form.name,
|
||||||
|
description: form.description,
|
||||||
metadataUri: form.metadataUri,
|
metadataUri: form.metadataUri,
|
||||||
})
|
})
|
||||||
await sendTx([ix as never], [['listings'], ['listing', editPk]], 'update_listing')
|
await sendTx([ix as never], [['listings'], ['listing', editPk]], 'update_listing')
|
||||||
@@ -275,6 +286,8 @@ export function CreateListingForm({ editPk }: Props) {
|
|||||||
altCurrencies,
|
altCurrencies,
|
||||||
acceptedResolvers,
|
acceptedResolvers,
|
||||||
quantity: quantityN,
|
quantity: quantityN,
|
||||||
|
name: form.name,
|
||||||
|
description: form.description,
|
||||||
metadataUri: form.metadataUri,
|
metadataUri: form.metadataUri,
|
||||||
})
|
})
|
||||||
await sendTx([ix as never], [['listings']], 'create_listing')
|
await sendTx([ix as never], [['listings']], 'create_listing')
|
||||||
@@ -334,6 +347,16 @@ export function CreateListingForm({ editPk }: Props) {
|
|||||||
gap: 22,
|
gap: 22,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* NAME + DESCRIPTION */}
|
||||||
|
<div>
|
||||||
|
<label style={LABEL}>NAME <span style={{ fontWeight: 400, textTransform: 'none' }}>(max 64 chars)</span></label>
|
||||||
|
<input value={form.name} onChange={set('name')} placeholder="e.g. Mechanical Keyboard Kit" style={INPUT} maxLength={64} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL}>DESCRIPTION <span style={{ fontWeight: 400, textTransform: 'none' }}>(max 256 chars)</span></label>
|
||||||
|
<input value={form.description} onChange={set('description')} placeholder="Short description of what you're selling" style={INPUT} maxLength={256} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* CANONICAL CURRENCY */}
|
{/* CANONICAL CURRENCY */}
|
||||||
<div>
|
<div>
|
||||||
<label style={LABEL}>CANONICAL CURRENCY</label>
|
<label style={LABEL}>CANONICAL CURRENCY</label>
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ export function Dashboard() {
|
|||||||
>
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
<span style={{ fontWeight: 600, fontSize: 15 }}>
|
<span style={{ fontWeight: 600, fontSize: 15 }}>
|
||||||
{l.data.metadataUri || abbrev(l.address)}
|
{l.data.name || `Listing #${String(l.data.listingId)}`}
|
||||||
</span>
|
</span>
|
||||||
<Badge color={sc.color} bg={sc.bg}>
|
<Badge color={sc.color} bg={sc.bg}>
|
||||||
{l.data.isActive ? 'Active' : 'Inactive'}
|
{l.data.isActive ? 'Active' : 'Inactive'}
|
||||||
@@ -182,7 +182,7 @@ export function Dashboard() {
|
|||||||
fontFamily: 'var(--font-mono)',
|
fontFamily: 'var(--font-mono)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
#{String(l.data.listingId)} · {fmtSol(l.data.price)} · {avail}/
|
{abbrev(l.address)} · {fmtSol(l.data.price)} · {avail}/
|
||||||
{String(l.data.quantity)} avail · {String(l.data.quantityReserved)} pending
|
{String(l.data.quantity)} avail · {String(l.data.quantityReserved)} pending
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
110
app/src/components/EscrowDetail.tsx
Normal file
110
app/src/components/EscrowDetail.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { isSome } from '@solana/kit'
|
||||||
|
import { escrowStateLabel } from '@descro/sdk'
|
||||||
|
import { EscrowState } from '@descro/sdk/src/generated/descro/src/generated/types/escrowState'
|
||||||
|
import { useEscrow } from '@/hooks/useEscrow'
|
||||||
|
import { useOrderByEscrow } from '@/hooks/useOrderByEscrow'
|
||||||
|
import { EscrowStateMachine } from '@/components/EscrowStateMachine'
|
||||||
|
import { FieldRow, MonoChip } from '@/components/ui/FieldRow'
|
||||||
|
import { Badge, STATUS_COLORS } from '@/components/ui/Badge'
|
||||||
|
import { abbrev, fmtSol } from '@/lib/format'
|
||||||
|
import type { Address } from '@solana/kit'
|
||||||
|
|
||||||
|
function escrowStatusKey(state: EscrowState): keyof typeof STATUS_COLORS {
|
||||||
|
switch (state) {
|
||||||
|
case EscrowState.AwaitingSellerConfirm: return 'awaitingConfirm'
|
||||||
|
case EscrowState.Active: return 'active'
|
||||||
|
case EscrowState.Disputed: return 'disputed'
|
||||||
|
case EscrowState.Complete: return 'complete'
|
||||||
|
default: return 'cancelled'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
pda: Address
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EscrowDetail({ pda }: Props) {
|
||||||
|
const router = useRouter()
|
||||||
|
const { data: escrow, isLoading, error } = useEscrow(pda)
|
||||||
|
const { data: order } = useOrderByEscrow(pda)
|
||||||
|
|
||||||
|
if (isLoading) return <div style={{ padding: 48, textAlign: 'center', color: 'var(--mut)' }}>Loading…</div>
|
||||||
|
if (error || !escrow) return <div style={{ padding: 48, textAlign: 'center', color: 'var(--mut)' }}>Escrow account not found.</div>
|
||||||
|
|
||||||
|
const ed = escrow.data
|
||||||
|
const stateLabel = escrowStateLabel(ed.state)
|
||||||
|
const sc = STATUS_COLORS[escrowStatusKey(ed.state)]
|
||||||
|
const resolverPk = isSome(ed.disputeResolver) ? ed.disputeResolver.value : null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/escrows')}
|
||||||
|
style={{ display: 'inline-flex', alignItems: 'center', gap: 6, color: 'var(--mut)', fontSize: 13, fontWeight: 500, marginBottom: 18 }}
|
||||||
|
>
|
||||||
|
← Escrows
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16, marginBottom: 24, flexWrap: 'wrap' }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '.14em', color: 'var(--mut)', marginBottom: 5 }}>
|
||||||
|
ESCROW ACCOUNT · #{String(ed.escrowId)}
|
||||||
|
</div>
|
||||||
|
<h1 style={{ margin: 0, fontSize: 26, fontWeight: 700, letterSpacing: '-.02em' }}>{abbrev(pda)}</h1>
|
||||||
|
</div>
|
||||||
|
<Badge color={sc.color} bg={sc.bg}>{stateLabel}</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EscrowStateMachine state={ed.state} />
|
||||||
|
|
||||||
|
<div style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', borderRadius: 'var(--radius)', padding: '8px 22px 16px' }}>
|
||||||
|
<div style={{ fontSize: 11, letterSpacing: '.13em', color: 'var(--mut)', fontWeight: 600, padding: '14px 0 4px' }}>
|
||||||
|
ESCROW ACCOUNT · descro
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FieldRow label="Address">
|
||||||
|
<MonoChip value={abbrev(pda)} onCopy={() => navigator.clipboard.writeText(pda)} />
|
||||||
|
</FieldRow>
|
||||||
|
<FieldRow label="State">
|
||||||
|
<Badge color={sc.color} bg={sc.bg}>{stateLabel}</Badge>
|
||||||
|
</FieldRow>
|
||||||
|
<FieldRow label="Escrow ID">
|
||||||
|
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 12.5 }}>#{String(ed.escrowId)}</span>
|
||||||
|
</FieldRow>
|
||||||
|
<FieldRow label="Seller">
|
||||||
|
<MonoChip value={abbrev(ed.seller)} onCopy={() => navigator.clipboard.writeText(ed.seller)} />
|
||||||
|
</FieldRow>
|
||||||
|
<FieldRow label="Buyer">
|
||||||
|
<MonoChip value={abbrev(ed.buyer)} onCopy={() => navigator.clipboard.writeText(ed.buyer)} />
|
||||||
|
</FieldRow>
|
||||||
|
<FieldRow label="Amount">
|
||||||
|
<span style={{ fontSize: 13.5, fontWeight: 600 }}>{fmtSol(ed.amount)}</span>
|
||||||
|
</FieldRow>
|
||||||
|
{resolverPk && (
|
||||||
|
<FieldRow label="Resolver">
|
||||||
|
<MonoChip
|
||||||
|
value={abbrev(resolverPk)}
|
||||||
|
onClick={() => router.push(`/resolver/${resolverPk}`)}
|
||||||
|
onCopy={() => navigator.clipboard.writeText(resolverPk)}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
|
)}
|
||||||
|
<FieldRow label="Linked Order" last>
|
||||||
|
{order
|
||||||
|
? (
|
||||||
|
<MonoChip
|
||||||
|
value={abbrev(order.address)}
|
||||||
|
onClick={() => router.push(`/order/${order.address}`)}
|
||||||
|
onCopy={() => navigator.clipboard.writeText(order.address)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: <span style={{ fontSize: 13, color: 'var(--mut)' }}>—</span>
|
||||||
|
}
|
||||||
|
</FieldRow>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -82,8 +82,8 @@ export function EscrowsTable() {
|
|||||||
borderRadius: 7,
|
borderRadius: 7,
|
||||||
fontSize: 12.5,
|
fontSize: 12.5,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: filter === f.key ? 'var(--tx)' : 'var(--mut)',
|
color: filter === f.key ? '#0b0613' : 'var(--mut)',
|
||||||
background: filter === f.key ? 'var(--bg2)' : 'transparent',
|
background: filter === f.key ? 'linear-gradient(135deg,var(--acc),var(--acc2))' : 'transparent',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
@@ -137,7 +137,7 @@ export function EscrowsTable() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={e.pda}
|
key={e.pda}
|
||||||
onClick={() => router.push(`/search?q=${e.pda}`)}
|
onClick={() => router.push(`/escrow/${e.pda}`)}
|
||||||
style={{
|
style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '1.2fr 1.4fr 1.4fr .9fr 1.3fr .9fr',
|
gridTemplateColumns: '1.2fr 1.4fr 1.4fr .9fr 1.3fr .9fr',
|
||||||
|
|||||||
@@ -54,30 +54,40 @@ export function ListingDetail({ pk, walletAddress, onUpdate, onClose, onPlaceOrd
|
|||||||
LISTING ACCOUNT · #{String(d.listingId)}
|
LISTING ACCOUNT · #{String(d.listingId)}
|
||||||
</div>
|
</div>
|
||||||
<h1 style={{ margin: 0, fontSize: 26, fontWeight: 700, letterSpacing: '-.02em' }}>
|
<h1 style={{ margin: 0, fontSize: 26, fontWeight: 700, letterSpacing: '-.02em' }}>
|
||||||
{d.metadataUri || abbrev(pk)}
|
{d.name || abbrev(pk)}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<Badge color={sc.color} bg={sc.bg}>{d.isActive ? 'Active' : 'Inactive'}</Badge>
|
<Badge color={sc.color} bg={sc.bg}>{d.isActive ? 'Active' : 'Inactive'}</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
{(d.description || d.metadataUri) && (
|
||||||
|
<p style={{ margin: '0 0 24px', fontSize: 14, color: 'var(--mut)', maxWidth: 620, lineHeight: 1.55 }}>
|
||||||
|
{d.description || (
|
||||||
|
<a href={d.metadataUri} target="_blank" rel="noreferrer" style={{ color: 'var(--acc2light)' }}>{d.metadataUri}</a>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr', gap: 18, alignItems: 'start', marginBottom: 24 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr', gap: 18, alignItems: 'start', marginBottom: 24 }}>
|
||||||
{/* Left: account fields */}
|
{/* Left: account fields */}
|
||||||
<div style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', borderRadius: 'var(--radius)', padding: '8px 22px 16px' }}>
|
<div style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', borderRadius: 'var(--radius)', padding: '8px 22px 16px' }}>
|
||||||
<div style={{ fontSize: 11, letterSpacing: '.13em', color: 'var(--mut)', fontWeight: 600, padding: '14px 0 4px' }}>ACCOUNT FIELDS</div>
|
<div style={{ fontSize: 11, letterSpacing: '.13em', color: 'var(--mut)', fontWeight: 600, padding: '14px 0 4px' }}>ACCOUNT FIELDS</div>
|
||||||
<FieldRow label="Listing ID"><span style={{ fontSize: 13.5, fontWeight: 600 }}>#{String(d.listingId)}</span></FieldRow>
|
<FieldRow label="Address"><MonoChip value={abbrev(pk)} onCopy={() => navigator.clipboard.writeText(pk)} /></FieldRow>
|
||||||
<FieldRow label="Seller"><MonoChip value={abbrev(d.seller)} onCopy={() => navigator.clipboard.writeText(d.seller)} onClick={() => router.push(`/search?q=${d.seller}`)} /></FieldRow>
|
<FieldRow label="Seller"><MonoChip value={abbrev(d.seller)} onCopy={() => navigator.clipboard.writeText(d.seller)} onClick={() => router.push(`/search?q=${d.seller}`)} /></FieldRow>
|
||||||
<FieldRow label="Price"><span style={{ fontSize: 13.5, fontWeight: 600 }}>{fmtListingPrice()}</span></FieldRow>
|
<FieldRow label="Listing ID"><span style={{ fontFamily: 'var(--font-mono)', fontSize: 13 }}>#{String(d.listingId)}</span></FieldRow>
|
||||||
<FieldRow label="Currency">
|
<FieldRow label="Canonical Currency">
|
||||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 12.5 }}>
|
<span style={{ fontWeight: 600, fontSize: 13 }}>
|
||||||
{d.canonicalCurrency.__kind === 'Sol' ? 'SOL (native)' : `SPL · ${abbrev((d.canonicalCurrency as { mint: string }).mint)}`}
|
{d.canonicalCurrency.__kind === 'Sol' ? 'SOL' : `SPL · ${abbrev((d.canonicalCurrency as { mint: string }).mint)}`}
|
||||||
</span>
|
</span>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
{canonicalOracleAddr && (
|
<FieldRow label="Price"><span style={{ fontWeight: 700, fontSize: 13.5 }}>{fmtListingPrice()}</span></FieldRow>
|
||||||
<FieldRow label="Oracle">
|
<FieldRow label="Canonical Oracle">
|
||||||
<MonoChip value={abbrev(canonicalOracleAddr)} onCopy={() => navigator.clipboard.writeText(canonicalOracleAddr)} />
|
{canonicalOracleAddr
|
||||||
</FieldRow>
|
? <MonoChip value={abbrev(canonicalOracleAddr)} onCopy={() => navigator.clipboard.writeText(canonicalOracleAddr)} />
|
||||||
)}
|
: <span style={{ fontSize: 13, color: 'var(--mut)' }}>None — priced directly</span>
|
||||||
|
}
|
||||||
|
</FieldRow>
|
||||||
<FieldRow label="Status"><Badge color={sc.color} bg={sc.bg}>{d.isActive ? 'Active' : 'Inactive'}</Badge></FieldRow>
|
<FieldRow label="Status"><Badge color={sc.color} bg={sc.bg}>{d.isActive ? 'Active' : 'Inactive'}</Badge></FieldRow>
|
||||||
|
<FieldRow label="Bump"><span style={{ fontFamily: 'var(--font-mono)', fontSize: 13, color: 'var(--mut)' }}>{d.bump}</span></FieldRow>
|
||||||
|
|
||||||
{d.metadataUri && (
|
{d.metadataUri && (
|
||||||
<>
|
<>
|
||||||
@@ -137,7 +147,7 @@ export function ListingDetail({ pk, walletAddress, onUpdate, onClose, onPlaceOrd
|
|||||||
<Button variant="danger" onClick={onClose}>Close Listing</Button>
|
<Button variant="danger" onClick={onClose}>Close Listing</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{canPlace && <Button onClick={onPlaceOrder}>Place Order</Button>}
|
{canPlace && <Button style={{ width: '100%', padding: 11 }} onClick={onPlaceOrder}>Place Order</Button>}
|
||||||
{!walletAddress && <div style={{ fontSize: 13, color: 'var(--mut)', lineHeight: 1.5 }}>Connect a wallet to place an order or manage this listing.</div>}
|
{!walletAddress && <div style={{ fontSize: 13, color: 'var(--mut)', lineHeight: 1.5 }}>Connect a wallet to place an order or manage this listing.</div>}
|
||||||
{walletAddress && !isSeller && !canPlace && <div style={{ fontSize: 13, color: 'var(--mut)', lineHeight: 1.5 }}>No actions available — listing is inactive or out of stock.</div>}
|
{walletAddress && !isSeller && !canPlace && <div style={{ fontSize: 13, color: 'var(--mut)', lineHeight: 1.5 }}>No actions available — listing is inactive or out of stock.</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -59,15 +59,27 @@ export function ListingsTable() {
|
|||||||
style={{ height: 38, width: 220, padding: '0 13px', background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 10, fontSize: 13, fontFamily: 'var(--font-mono)', outline: 'none' }}
|
style={{ height: 38, width: 220, padding: '0 13px', background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 10, fontSize: 13, fontFamily: 'var(--font-mono)', outline: 'none' }}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: 'flex', background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 10, padding: 3 }}>
|
<div style={{ display: 'flex', background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 10, padding: 3 }}>
|
||||||
{FILTERS.map((f) => (
|
{FILTERS.map((f) => {
|
||||||
<button
|
const active = filter === f.key
|
||||||
key={f.key}
|
return (
|
||||||
onClick={() => setFilter(f.key)}
|
<button
|
||||||
style={{ padding: '6px 13px', borderRadius: 7, fontSize: 12.5, fontWeight: 600, color: filter === f.key ? 'var(--tx)' : 'var(--mut)', background: filter === f.key ? 'var(--bg2)' : 'transparent' }}
|
key={f.key}
|
||||||
>
|
onClick={() => setFilter(f.key)}
|
||||||
{f.label}
|
style={{
|
||||||
</button>
|
padding: '6px 13px',
|
||||||
))}
|
borderRadius: 7,
|
||||||
|
fontSize: 12.5,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: active ? '#0b0613' : 'var(--mut)',
|
||||||
|
background: active ? 'linear-gradient(135deg,var(--acc),var(--acc2))' : 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{f.label}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,7 +108,7 @@ export function ListingsTable() {
|
|||||||
>
|
>
|
||||||
<div style={{ minWidth: 0 }}>
|
<div style={{ minWidth: 0 }}>
|
||||||
<div style={{ fontWeight: 600, fontSize: 14, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
<div style={{ fontWeight: 600, fontSize: 14, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||||
{l.data.metadataUri || abbrev(l.address)}
|
{l.data.name || abbrev(l.address)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--mut)', marginTop: 3 }}>
|
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--mut)', marginTop: 3 }}>
|
||||||
#{String(l.data.listingId)} · {abbrev(l.address)}
|
#{String(l.data.listingId)} · {abbrev(l.address)}
|
||||||
|
|||||||
@@ -147,6 +147,9 @@ export function Nav() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Spacer — pushes right-side controls to the edge */}
|
||||||
|
<div style={{ flex: 1 }} />
|
||||||
|
|
||||||
{/* Network picker */}
|
{/* Network picker */}
|
||||||
<div ref={netRef} style={{ position: 'relative', flexShrink: 0 }}>
|
<div ref={netRef} style={{ position: 'relative', flexShrink: 0 }}>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function OrderDetail({ pk, walletAddress, onAccept, onReject, onCancel, o
|
|||||||
const od = order.data
|
const od = order.data
|
||||||
const ed = escrow?.data
|
const ed = escrow?.data
|
||||||
|
|
||||||
const stateEnum = ed?.state ?? EscrowState.Cancelled
|
const stateEnum = ed?.state ?? EscrowState.Complete
|
||||||
const stateLabel = escrowStateLabel(stateEnum)
|
const stateLabel = escrowStateLabel(stateEnum)
|
||||||
const sc = STATUS_COLORS[escrowStatusKey(stateEnum)]
|
const sc = STATUS_COLORS[escrowStatusKey(stateEnum)]
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ export function Button({ variant = 'primary', children, style, disabled, ...rest
|
|||||||
{...rest}
|
{...rest}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
style={{
|
style={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
padding: '11px 18px',
|
padding: '11px 18px',
|
||||||
borderRadius: 'var(--radius)',
|
borderRadius: 'var(--radius)',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
|
|||||||
@@ -2,24 +2,26 @@
|
|||||||
|
|
||||||
import { createContext, useCallback, useContext, useState, type ReactNode } from 'react'
|
import { createContext, useCallback, useContext, useState, type ReactNode } from 'react'
|
||||||
|
|
||||||
const ToastCtx = createContext<(msg: string) => void>(() => {})
|
type ToastFn = (msg: string, type?: 'success' | 'error') => void
|
||||||
|
|
||||||
|
const ToastCtx = createContext<ToastFn>(() => {})
|
||||||
|
|
||||||
export function useToast() {
|
export function useToast() {
|
||||||
return useContext(ToastCtx)
|
return useContext(ToastCtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToastProvider({ children }: { children: ReactNode }) {
|
export function ToastProvider({ children }: { children: ReactNode }) {
|
||||||
const [msg, setMsg] = useState('')
|
const [state, setState] = useState<{ msg: string; type: 'success' | 'error' } | null>(null)
|
||||||
|
|
||||||
const show = useCallback((m: string) => {
|
const show = useCallback<ToastFn>((m, type = 'success') => {
|
||||||
setMsg(m)
|
setState({ msg: m, type })
|
||||||
setTimeout(() => setMsg(''), 2800)
|
setTimeout(() => setState(null), type === 'error' ? 4500 : 2800)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastCtx.Provider value={show}>
|
<ToastCtx.Provider value={show}>
|
||||||
{children}
|
{children}
|
||||||
{msg && (
|
{state && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
@@ -32,7 +34,7 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
|||||||
gap: 10,
|
gap: 10,
|
||||||
padding: '12px 18px',
|
padding: '12px 18px',
|
||||||
background: 'var(--bg2)',
|
background: 'var(--bg2)',
|
||||||
border: '1px solid var(--accBd)',
|
border: `1px solid ${state.type === 'error' ? 'var(--errBd, #f87171)' : 'var(--accBd)'}`,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
boxShadow: '0 16px 40px rgba(0,0,0,.55)',
|
boxShadow: '0 16px 40px rgba(0,0,0,.55)',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
@@ -46,12 +48,12 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
|||||||
width: 8,
|
width: 8,
|
||||||
height: 8,
|
height: 8,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
background: 'var(--acc2)',
|
background: state.type === 'error' ? '#f87171' : 'var(--acc2)',
|
||||||
boxShadow: '0 0 8px var(--acc2)',
|
boxShadow: state.type === 'error' ? '0 0 8px #f87171' : '0 0 8px var(--acc2)',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{msg}
|
{state.msg}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ToastCtx.Provider>
|
</ToastCtx.Provider>
|
||||||
|
|||||||
21
app/src/hooks/useEscrow.ts
Normal file
21
app/src/hooks/useEscrow.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { useCluster } from '@solana/connector/react'
|
||||||
|
import { createSolanaRpc } from '@solana/kit'
|
||||||
|
import { fetchEscrowAccount } from '@descro/sdk'
|
||||||
|
import type { Address } from '@solana/kit'
|
||||||
|
|
||||||
|
export function useEscrow(pda: Address) {
|
||||||
|
const { cluster } = useCluster()
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['escrow', pda, cluster?.id],
|
||||||
|
queryFn: () => {
|
||||||
|
const rpc = createSolanaRpc(cluster!.url)
|
||||||
|
return fetchEscrowAccount(
|
||||||
|
rpc as Parameters<typeof fetchEscrowAccount>[0],
|
||||||
|
pda,
|
||||||
|
).catch(() => null)
|
||||||
|
},
|
||||||
|
enabled: !!cluster && !!pda,
|
||||||
|
refetchInterval: 15_000,
|
||||||
|
})
|
||||||
|
}
|
||||||
18
app/src/hooks/useOrderByEscrow.ts
Normal file
18
app/src/hooks/useOrderByEscrow.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { useCluster } from '@solana/connector/react'
|
||||||
|
import { createSolanaRpc } from '@solana/kit'
|
||||||
|
import { fetchOrderByEscrow } from '@solisting/sdk'
|
||||||
|
import type { Address } from '@solana/kit'
|
||||||
|
|
||||||
|
export function useOrderByEscrow(escrowPda: Address) {
|
||||||
|
const { cluster } = useCluster()
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['orderByEscrow', escrowPda, cluster?.id],
|
||||||
|
queryFn: () => {
|
||||||
|
const rpc = createSolanaRpc(cluster!.url)
|
||||||
|
return fetchOrderByEscrow(rpc, escrowPda)
|
||||||
|
},
|
||||||
|
enabled: !!cluster && !!escrowPda,
|
||||||
|
refetchInterval: 30_000,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -39,10 +39,15 @@ export function useTx() {
|
|||||||
|
|
||||||
const signed = await signTransactionMessageWithSigners(txMsg)
|
const signed = await signTransactionMessageWithSigners(txMsg)
|
||||||
assertIsTransactionWithBlockhashLifetime(signed)
|
assertIsTransactionWithBlockhashLifetime(signed)
|
||||||
await sendAndConfirmTransactionFactory({ rpc: rpc as never, rpcSubscriptions: rpcSubscriptions as never })(
|
try {
|
||||||
signed as never,
|
await sendAndConfirmTransactionFactory({ rpc: rpc as never, rpcSubscriptions: rpcSubscriptions as never })(
|
||||||
{ commitment: 'confirmed' },
|
signed as never,
|
||||||
)
|
{ commitment: 'confirmed' },
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
toast(friendlyTxError(err), 'error')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
toast(`Tx confirmed — ${label}`)
|
toast(`Tx confirmed — ${label}`)
|
||||||
for (const key of invalidateKeys) {
|
for (const key of invalidateKeys) {
|
||||||
@@ -50,3 +55,20 @@ export function useTx() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function friendlyTxError(err: unknown): string {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err)
|
||||||
|
if (
|
||||||
|
msg.includes('Attempt to debit an account but found no record of a prior credit') ||
|
||||||
|
msg.includes('AccountNotFound')
|
||||||
|
) {
|
||||||
|
return 'Insufficient SOL — please fund your wallet and try again.'
|
||||||
|
}
|
||||||
|
if (msg.includes('insufficient lamports') || msg.includes('insufficient funds')) {
|
||||||
|
return 'Insufficient SOL — please fund your wallet and try again.'
|
||||||
|
}
|
||||||
|
if (msg.includes('User rejected') || msg.includes('Transaction was not confirmed')) {
|
||||||
|
return 'Transaction cancelled.'
|
||||||
|
}
|
||||||
|
return 'Transaction failed — please try again.'
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ pub struct AcceptOrder<'info> {
|
|||||||
seeds = [b"order", order_account.listing.as_ref(), order_account.buyer.as_ref(), &order_account.order_id.to_le_bytes()],
|
seeds = [b"order", order_account.listing.as_ref(), order_account.buyer.as_ref(), &order_account.order_id.to_le_bytes()],
|
||||||
bump = order_account.bump,
|
bump = order_account.bump,
|
||||||
constraint = seller.key() == order_account.seller @ SolistingError::Unauthorized,
|
constraint = seller.key() == order_account.seller @ SolistingError::Unauthorized,
|
||||||
close = seller,
|
|
||||||
)]
|
)]
|
||||||
pub order_account: Account<'info, OrderAccount>,
|
pub order_account: Account<'info, OrderAccount>,
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ pub fn handler(
|
|||||||
alt_currencies: Vec<AltCurrencyConfig>,
|
alt_currencies: Vec<AltCurrencyConfig>,
|
||||||
accepted_resolvers: Vec<Pubkey>,
|
accepted_resolvers: Vec<Pubkey>,
|
||||||
quantity: u32,
|
quantity: u32,
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
metadata_uri: String,
|
metadata_uri: String,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let listing = &mut ctx.accounts.listing_account;
|
let listing = &mut ctx.accounts.listing_account;
|
||||||
@@ -40,6 +42,8 @@ pub fn handler(
|
|||||||
listing.accepted_resolvers = accepted_resolvers;
|
listing.accepted_resolvers = accepted_resolvers;
|
||||||
listing.quantity = quantity;
|
listing.quantity = quantity;
|
||||||
listing.quantity_reserved = 0;
|
listing.quantity_reserved = 0;
|
||||||
|
listing.name = name;
|
||||||
|
listing.description = description;
|
||||||
listing.metadata_uri = metadata_uri;
|
listing.metadata_uri = metadata_uri;
|
||||||
listing.listing_id = listing_id;
|
listing.listing_id = listing_id;
|
||||||
listing.is_active = true;
|
listing.is_active = true;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::state::{Currency, ListingAccount, OrderAccount};
|
|||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
#[instruction(order_id: u64, escrow_id: u64, resolver: Pubkey, payment_currency: Currency)]
|
#[instruction(order_id: u64, escrow_id: u64, resolver: Option<Pubkey>, payment_currency: Currency)]
|
||||||
pub struct CreateOrder<'info> {
|
pub struct CreateOrder<'info> {
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub buyer: Signer<'info>,
|
pub buyer: Signer<'info>,
|
||||||
@@ -67,7 +67,7 @@ pub fn handler(
|
|||||||
ctx: Context<CreateOrder>,
|
ctx: Context<CreateOrder>,
|
||||||
order_id: u64,
|
order_id: u64,
|
||||||
escrow_id: u64,
|
escrow_id: u64,
|
||||||
resolver: Pubkey,
|
resolver: Option<Pubkey>,
|
||||||
payment_currency: Currency,
|
payment_currency: Currency,
|
||||||
expected_amount: u64,
|
expected_amount: u64,
|
||||||
max_slippage_bps: u16,
|
max_slippage_bps: u16,
|
||||||
@@ -80,10 +80,13 @@ pub fn handler(
|
|||||||
let listing = &ctx.accounts.listing_account;
|
let listing = &ctx.accounts.listing_account;
|
||||||
|
|
||||||
if !listing.accepted_resolvers.is_empty() {
|
if !listing.accepted_resolvers.is_empty() {
|
||||||
require!(
|
match resolver {
|
||||||
listing.accepted_resolvers.contains(&resolver),
|
Some(r) => require!(
|
||||||
SolistingError::ResolverNotAccepted
|
listing.accepted_resolvers.contains(&r),
|
||||||
);
|
SolistingError::ResolverNotAccepted
|
||||||
|
),
|
||||||
|
None => return err!(SolistingError::ResolverNotAccepted),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let amount = if payment_currency == listing.canonical_currency {
|
let amount = if payment_currency == listing.canonical_currency {
|
||||||
@@ -143,7 +146,7 @@ pub fn handler(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
amount,
|
amount,
|
||||||
Some(resolver),
|
resolver,
|
||||||
escrow_id,
|
escrow_id,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ pub fn handler(
|
|||||||
alt_currencies: Vec<AltCurrencyConfig>,
|
alt_currencies: Vec<AltCurrencyConfig>,
|
||||||
accepted_resolvers: Vec<Pubkey>,
|
accepted_resolvers: Vec<Pubkey>,
|
||||||
quantity: u32,
|
quantity: u32,
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
metadata_uri: String,
|
metadata_uri: String,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let listing = &mut ctx.accounts.listing_account;
|
let listing = &mut ctx.accounts.listing_account;
|
||||||
@@ -34,6 +36,8 @@ pub fn handler(
|
|||||||
listing.alt_currencies = alt_currencies;
|
listing.alt_currencies = alt_currencies;
|
||||||
listing.accepted_resolvers = accepted_resolvers;
|
listing.accepted_resolvers = accepted_resolvers;
|
||||||
listing.quantity = quantity;
|
listing.quantity = quantity;
|
||||||
|
listing.name = name;
|
||||||
|
listing.description = description;
|
||||||
listing.metadata_uri = metadata_uri;
|
listing.metadata_uri = metadata_uri;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,11 +31,14 @@ pub mod solisting {
|
|||||||
alt_currencies: Vec<state::AltCurrencyConfig>,
|
alt_currencies: Vec<state::AltCurrencyConfig>,
|
||||||
accepted_resolvers: Vec<Pubkey>,
|
accepted_resolvers: Vec<Pubkey>,
|
||||||
quantity: u32,
|
quantity: u32,
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
metadata_uri: String,
|
metadata_uri: String,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
create_listing::handler(
|
create_listing::handler(
|
||||||
ctx, listing_id, canonical_currency, price,
|
ctx, listing_id, canonical_currency, price,
|
||||||
canonical_oracle, alt_currencies, accepted_resolvers, quantity, metadata_uri,
|
canonical_oracle, alt_currencies, accepted_resolvers, quantity,
|
||||||
|
name, description, metadata_uri,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,11 +51,14 @@ pub mod solisting {
|
|||||||
alt_currencies: Vec<state::AltCurrencyConfig>,
|
alt_currencies: Vec<state::AltCurrencyConfig>,
|
||||||
accepted_resolvers: Vec<Pubkey>,
|
accepted_resolvers: Vec<Pubkey>,
|
||||||
quantity: u32,
|
quantity: u32,
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
metadata_uri: String,
|
metadata_uri: String,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
update_listing::handler(
|
update_listing::handler(
|
||||||
ctx, canonical_currency, price,
|
ctx, canonical_currency, price,
|
||||||
canonical_oracle, alt_currencies, accepted_resolvers, quantity, metadata_uri,
|
canonical_oracle, alt_currencies, accepted_resolvers, quantity,
|
||||||
|
name, description, metadata_uri,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +70,7 @@ pub mod solisting {
|
|||||||
ctx: Context<CreateOrder>,
|
ctx: Context<CreateOrder>,
|
||||||
order_id: u64,
|
order_id: u64,
|
||||||
escrow_id: u64,
|
escrow_id: u64,
|
||||||
resolver: Pubkey,
|
resolver: Option<Pubkey>,
|
||||||
payment_currency: state::Currency,
|
payment_currency: state::Currency,
|
||||||
expected_amount: u64,
|
expected_amount: u64,
|
||||||
max_slippage_bps: u16,
|
max_slippage_bps: u16,
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ pub struct ListingAccount {
|
|||||||
pub quantity: u32,
|
pub quantity: u32,
|
||||||
/// Units held by pending orders. available = quantity - quantity_reserved.
|
/// Units held by pending orders. available = quantity - quantity_reserved.
|
||||||
pub quantity_reserved: u32,
|
pub quantity_reserved: u32,
|
||||||
|
#[max_len(64)]
|
||||||
|
pub name: String,
|
||||||
|
#[max_len(256)]
|
||||||
|
pub description: String,
|
||||||
#[max_len(256)]
|
#[max_len(256)]
|
||||||
pub metadata_uri: String,
|
pub metadata_uri: String,
|
||||||
pub listing_id: u64,
|
pub listing_id: u64,
|
||||||
@@ -65,7 +69,7 @@ pub struct OrderAccount {
|
|||||||
pub listing: Pubkey,
|
pub listing: Pubkey,
|
||||||
pub buyer: Pubkey,
|
pub buyer: Pubkey,
|
||||||
pub seller: Pubkey,
|
pub seller: Pubkey,
|
||||||
pub resolver: Pubkey,
|
pub resolver: Option<Pubkey>,
|
||||||
/// The currency the buyer chose to pay in.
|
/// The currency the buyer chose to pay in.
|
||||||
pub payment_currency: Currency,
|
pub payment_currency: Currency,
|
||||||
/// Actual amount paid (may differ from price when oracle-converted).
|
/// Actual amount paid (may differ from price when oracle-converted).
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ pub fn ix_create_listing(
|
|||||||
alt_currencies: Vec<AltCurrencyConfig>,
|
alt_currencies: Vec<AltCurrencyConfig>,
|
||||||
accepted_resolvers: Vec<Pubkey>,
|
accepted_resolvers: Vec<Pubkey>,
|
||||||
quantity: u32,
|
quantity: u32,
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
metadata_uri: String,
|
metadata_uri: String,
|
||||||
) -> Instruction {
|
) -> Instruction {
|
||||||
let listing_account = listing_pda(seller, listing_id);
|
let listing_account = listing_pda(seller, listing_id);
|
||||||
@@ -114,6 +116,8 @@ pub fn ix_create_listing(
|
|||||||
alt_currencies,
|
alt_currencies,
|
||||||
accepted_resolvers,
|
accepted_resolvers,
|
||||||
quantity,
|
quantity,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
metadata_uri,
|
metadata_uri,
|
||||||
}
|
}
|
||||||
.data(),
|
.data(),
|
||||||
@@ -136,6 +140,8 @@ pub fn ix_update_listing(
|
|||||||
alt_currencies: Vec<AltCurrencyConfig>,
|
alt_currencies: Vec<AltCurrencyConfig>,
|
||||||
accepted_resolvers: Vec<Pubkey>,
|
accepted_resolvers: Vec<Pubkey>,
|
||||||
quantity: u32,
|
quantity: u32,
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
metadata_uri: String,
|
metadata_uri: String,
|
||||||
) -> Instruction {
|
) -> Instruction {
|
||||||
let listing_account = listing_pda(seller, listing_id);
|
let listing_account = listing_pda(seller, listing_id);
|
||||||
@@ -148,6 +154,8 @@ pub fn ix_update_listing(
|
|||||||
alt_currencies,
|
alt_currencies,
|
||||||
accepted_resolvers,
|
accepted_resolvers,
|
||||||
quantity,
|
quantity,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
metadata_uri,
|
metadata_uri,
|
||||||
}
|
}
|
||||||
.data(),
|
.data(),
|
||||||
@@ -187,7 +195,6 @@ pub fn ix_create_order(
|
|||||||
canonical_oracle: Option<Pubkey>,
|
canonical_oracle: Option<Pubkey>,
|
||||||
target_oracle: Option<Pubkey>,
|
target_oracle: Option<Pubkey>,
|
||||||
) -> Instruction {
|
) -> Instruction {
|
||||||
let resolver = Pubkey::new_unique();
|
|
||||||
ix_create_order_with_resolver(
|
ix_create_order_with_resolver(
|
||||||
buyer,
|
buyer,
|
||||||
seller,
|
seller,
|
||||||
@@ -197,7 +204,7 @@ pub fn ix_create_order(
|
|||||||
max_slippage_bps,
|
max_slippage_bps,
|
||||||
canonical_oracle,
|
canonical_oracle,
|
||||||
target_oracle,
|
target_oracle,
|
||||||
resolver,
|
Some(Pubkey::new_unique()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +218,7 @@ pub fn ix_create_order_with_resolver(
|
|||||||
max_slippage_bps: u16,
|
max_slippage_bps: u16,
|
||||||
canonical_oracle: Option<Pubkey>,
|
canonical_oracle: Option<Pubkey>,
|
||||||
target_oracle: Option<Pubkey>,
|
target_oracle: Option<Pubkey>,
|
||||||
resolver: Pubkey,
|
resolver: Option<Pubkey>,
|
||||||
) -> Instruction {
|
) -> Instruction {
|
||||||
let listing = listing_pda(seller, listing_id);
|
let listing = listing_pda(seller, listing_id);
|
||||||
let order = order_pda(listing, *buyer, order_id);
|
let order = order_pda(listing, *buyer, order_id);
|
||||||
@@ -256,7 +263,7 @@ pub fn ix_accept_order(
|
|||||||
buyer: &Pubkey,
|
buyer: &Pubkey,
|
||||||
order_id: u64,
|
order_id: u64,
|
||||||
escrow_id: u64,
|
escrow_id: u64,
|
||||||
resolver: Pubkey,
|
resolver: Option<Pubkey>,
|
||||||
) -> Instruction {
|
) -> Instruction {
|
||||||
let listing_account = listing_pda(seller, listing_id);
|
let listing_account = listing_pda(seller, listing_id);
|
||||||
let order_account = order_pda(listing_account, *buyer, order_id);
|
let order_account = order_pda(listing_account, *buyer, order_id);
|
||||||
@@ -268,7 +275,7 @@ pub fn ix_accept_order(
|
|||||||
&solisting::instruction::AcceptOrder {}.data(),
|
&solisting::instruction::AcceptOrder {}.data(),
|
||||||
solisting::accounts::AcceptOrder {
|
solisting::accounts::AcceptOrder {
|
||||||
seller: *seller,
|
seller: *seller,
|
||||||
resolver,
|
resolver: resolver.unwrap_or(system_program::ID),
|
||||||
listing_account,
|
listing_account,
|
||||||
order_account,
|
order_account,
|
||||||
escrow_account,
|
escrow_account,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ fn seller_can_create_sol_only_listing() {
|
|||||||
vec![],
|
vec![],
|
||||||
vec![],
|
vec![],
|
||||||
10,
|
10,
|
||||||
"ipfs://test".to_string(),
|
"".to_string(), "".to_string(), "ipfs://test".to_string(),
|
||||||
);
|
);
|
||||||
send(&mut svm, &[ix], &[&seller]);
|
send(&mut svm, &[ix], &[&seller]);
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ fn seller_can_create_listing_with_usdc_alt_stablecoin() {
|
|||||||
}],
|
}],
|
||||||
vec![],
|
vec![],
|
||||||
5,
|
5,
|
||||||
"ipfs://x".to_string(),
|
"".to_string(), "".to_string(), "ipfs://x".to_string(),
|
||||||
);
|
);
|
||||||
send(&mut svm, &[ix], &[&seller]);
|
send(&mut svm, &[ix], &[&seller]);
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ fn seller_can_create_listing_with_bonk_alt_oracle() {
|
|||||||
}],
|
}],
|
||||||
vec![],
|
vec![],
|
||||||
5,
|
5,
|
||||||
"".to_string(),
|
"".to_string(), "".to_string(), "".to_string(),
|
||||||
);
|
);
|
||||||
send(&mut svm, &[ix], &[&seller]);
|
send(&mut svm, &[ix], &[&seller]);
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ fn seller_can_update_listing() {
|
|||||||
let listing_id: u64 = 2;
|
let listing_id: u64 = 2;
|
||||||
let ix = ix_create_listing(
|
let ix = ix_create_listing(
|
||||||
&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000,
|
&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000,
|
||||||
None, vec![], vec![], 10, "".to_string(),
|
None, vec![], vec![], 10, "".to_string(), "".to_string(), "".to_string(),
|
||||||
);
|
);
|
||||||
send(&mut svm, &[ix], &[&seller]);
|
send(&mut svm, &[ix], &[&seller]);
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ fn seller_can_update_listing() {
|
|||||||
vec![],
|
vec![],
|
||||||
vec![],
|
vec![],
|
||||||
20,
|
20,
|
||||||
"ipfs://new".to_string(),
|
"".to_string(), "".to_string(), "ipfs://new".to_string(),
|
||||||
);
|
);
|
||||||
send(&mut svm, &[ix_update], &[&seller]);
|
send(&mut svm, &[ix_update], &[&seller]);
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ fn seller_can_close_listing() {
|
|||||||
let listing_id: u64 = 3;
|
let listing_id: u64 = 3;
|
||||||
let ix = ix_create_listing(
|
let ix = ix_create_listing(
|
||||||
&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000,
|
&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000,
|
||||||
None, vec![], vec![], 5, "".to_string(),
|
None, vec![], vec![], 5, "".to_string(), "".to_string(), "".to_string(),
|
||||||
);
|
);
|
||||||
send(&mut svm, &[ix], &[&seller]);
|
send(&mut svm, &[ix], &[&seller]);
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ fn buyer_can_create_order_canonical_sol() {
|
|||||||
let price = 100_000_000u64;
|
let price = 100_000_000u64;
|
||||||
let ix = ix_create_listing(
|
let ix = ix_create_listing(
|
||||||
&seller.pubkey(), listing_id, Currency::Sol, price,
|
&seller.pubkey(), listing_id, Currency::Sol, price,
|
||||||
None, vec![], vec![], 5, "".to_string(),
|
None, vec![], vec![], 5, "".to_string(), "".to_string(), "".to_string(),
|
||||||
);
|
);
|
||||||
send(&mut svm, &[ix], &[&seller]);
|
send(&mut svm, &[ix], &[&seller]);
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ fn create_order_fails_if_listing_inactive() {
|
|||||||
let listing_id = 2u64;
|
let listing_id = 2u64;
|
||||||
let ix = ix_create_listing(
|
let ix = ix_create_listing(
|
||||||
&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000,
|
&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000,
|
||||||
None, vec![], vec![], 1, "".to_string(),
|
None, vec![], vec![], 1, "".to_string(), "".to_string(), "".to_string(),
|
||||||
);
|
);
|
||||||
send(&mut svm, &[ix], &[&seller]);
|
send(&mut svm, &[ix], &[&seller]);
|
||||||
let ix_close = ix_close_listing(&seller.pubkey(), listing_id);
|
let ix_close = ix_close_listing(&seller.pubkey(), listing_id);
|
||||||
@@ -62,7 +62,7 @@ fn create_order_fails_if_out_of_stock() {
|
|||||||
let listing_id = 3u64;
|
let listing_id = 3u64;
|
||||||
let ix = ix_create_listing(
|
let ix = ix_create_listing(
|
||||||
&seller.pubkey(), listing_id, Currency::Sol, 100_000_000,
|
&seller.pubkey(), listing_id, Currency::Sol, 100_000_000,
|
||||||
None, vec![], vec![], 1, "".to_string(),
|
None, vec![], vec![], 1, "".to_string(), "".to_string(), "".to_string(),
|
||||||
);
|
);
|
||||||
send(&mut svm, &[ix], &[&seller]);
|
send(&mut svm, &[ix], &[&seller]);
|
||||||
send(
|
send(
|
||||||
@@ -87,7 +87,7 @@ fn create_order_fails_if_currency_not_accepted() {
|
|||||||
let listing_id = 4u64;
|
let listing_id = 4u64;
|
||||||
let ix = ix_create_listing(
|
let ix = ix_create_listing(
|
||||||
&seller.pubkey(), listing_id, Currency::Sol, 100_000_000,
|
&seller.pubkey(), listing_id, Currency::Sol, 100_000_000,
|
||||||
None, vec![], vec![], 5, "".to_string(),
|
None, vec![], vec![], 5, "".to_string(), "".to_string(), "".to_string(),
|
||||||
);
|
);
|
||||||
send(&mut svm, &[ix], &[&seller]);
|
send(&mut svm, &[ix], &[&seller]);
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ fn create_order_fails_if_resolver_not_accepted() {
|
|||||||
let allowed_resolver = Pubkey::new_unique();
|
let allowed_resolver = Pubkey::new_unique();
|
||||||
let ix = ix_create_listing(
|
let ix = ix_create_listing(
|
||||||
&seller.pubkey(), listing_id, Currency::Sol, 100_000_000,
|
&seller.pubkey(), listing_id, Currency::Sol, 100_000_000,
|
||||||
None, vec![], vec![allowed_resolver], 5, "".to_string(),
|
None, vec![], vec![allowed_resolver], 5, "".to_string(), "".to_string(), "".to_string(),
|
||||||
);
|
);
|
||||||
send(&mut svm, &[ix], &[&seller]);
|
send(&mut svm, &[ix], &[&seller]);
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ fn create_order_fails_if_resolver_not_accepted() {
|
|||||||
&mut svm,
|
&mut svm,
|
||||||
&[ix_create_order_with_resolver(
|
&[ix_create_order_with_resolver(
|
||||||
&buyer.pubkey(), &seller.pubkey(), listing_id, 1,
|
&buyer.pubkey(), &seller.pubkey(), listing_id, 1,
|
||||||
Currency::Sol, 0, None, None, Pubkey::new_unique(),
|
Currency::Sol, 0, None, None, Some(Pubkey::new_unique()),
|
||||||
)],
|
)],
|
||||||
&[&buyer],
|
&[&buyer],
|
||||||
);
|
);
|
||||||
@@ -132,7 +132,7 @@ fn seller_accept_creates_active_descro_escrow() {
|
|||||||
let price = 100_000_000u64;
|
let price = 100_000_000u64;
|
||||||
send(
|
send(
|
||||||
&mut svm,
|
&mut svm,
|
||||||
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, None, vec![], vec![], 5, "".to_string())],
|
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, None, vec![], vec![], 5, "".to_string(), "".to_string(), "".to_string())],
|
||||||
&[&seller],
|
&[&seller],
|
||||||
);
|
);
|
||||||
let order_id = 1u64;
|
let order_id = 1u64;
|
||||||
@@ -152,7 +152,7 @@ fn seller_accept_creates_active_descro_escrow() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let order_key = order_pda(listing_key, buyer.pubkey(), order_id);
|
let order_key = order_pda(listing_key, buyer.pubkey(), order_id);
|
||||||
assert!(svm.get_account(&order_key).is_none());
|
assert!(svm.get_account(&order_key).is_some());
|
||||||
|
|
||||||
let listing = read_listing(&svm, &seller.pubkey(), listing_id);
|
let listing = read_listing(&svm, &seller.pubkey(), listing_id);
|
||||||
assert_eq!(listing.quantity_reserved, 0);
|
assert_eq!(listing.quantity_reserved, 0);
|
||||||
@@ -166,7 +166,7 @@ fn seller_can_reject_order() {
|
|||||||
let price = 100_000_000u64;
|
let price = 100_000_000u64;
|
||||||
send(
|
send(
|
||||||
&mut svm,
|
&mut svm,
|
||||||
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, None, vec![], vec![], 5, "".to_string())],
|
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, None, vec![], vec![], 5, "".to_string(), "".to_string(), "".to_string())],
|
||||||
&[&seller],
|
&[&seller],
|
||||||
);
|
);
|
||||||
send(
|
send(
|
||||||
@@ -197,7 +197,7 @@ fn buyer_can_cancel_order() {
|
|||||||
let listing_id = 21u64;
|
let listing_id = 21u64;
|
||||||
send(
|
send(
|
||||||
&mut svm,
|
&mut svm,
|
||||||
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string())],
|
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string(), "".to_string(), "".to_string())],
|
||||||
&[&seller],
|
&[&seller],
|
||||||
);
|
);
|
||||||
send(
|
send(
|
||||||
@@ -223,7 +223,7 @@ fn reject_handles_already_cancelled_escrow() {
|
|||||||
let listing_id = 22u64;
|
let listing_id = 22u64;
|
||||||
send(
|
send(
|
||||||
&mut svm,
|
&mut svm,
|
||||||
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string())],
|
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string(), "".to_string(), "".to_string())],
|
||||||
&[&seller],
|
&[&seller],
|
||||||
);
|
);
|
||||||
send(
|
send(
|
||||||
@@ -250,7 +250,7 @@ fn anyone_can_close_stale_order_after_terminal_escrow() {
|
|||||||
let listing_id = 30u64;
|
let listing_id = 30u64;
|
||||||
send(
|
send(
|
||||||
&mut svm,
|
&mut svm,
|
||||||
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string())],
|
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string(), "".to_string(), "".to_string())],
|
||||||
&[&seller],
|
&[&seller],
|
||||||
);
|
);
|
||||||
send(
|
send(
|
||||||
@@ -284,7 +284,7 @@ fn close_stale_order_fails_if_escrow_still_active() {
|
|||||||
let listing_id = 31u64;
|
let listing_id = 31u64;
|
||||||
send(
|
send(
|
||||||
&mut svm,
|
&mut svm,
|
||||||
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string())],
|
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string(), "".to_string(), "".to_string())],
|
||||||
&[&seller],
|
&[&seller],
|
||||||
);
|
);
|
||||||
send(
|
send(
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ export type SolistingStateListingAccount = {
|
|||||||
quantity: number;
|
quantity: number;
|
||||||
/** Units held by pending orders. available = quantity - quantity_reserved. */
|
/** Units held by pending orders. available = quantity - quantity_reserved. */
|
||||||
quantityReserved: number;
|
quantityReserved: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
metadataUri: string;
|
metadataUri: string;
|
||||||
listingId: bigint;
|
listingId: bigint;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@@ -125,6 +127,8 @@ export type SolistingStateListingAccountArgs = {
|
|||||||
quantity: number;
|
quantity: number;
|
||||||
/** Units held by pending orders. available = quantity - quantity_reserved. */
|
/** Units held by pending orders. available = quantity - quantity_reserved. */
|
||||||
quantityReserved: number;
|
quantityReserved: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
metadataUri: string;
|
metadataUri: string;
|
||||||
listingId: number | bigint;
|
listingId: number | bigint;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@@ -147,6 +151,8 @@ export function getSolistingStateListingAccountEncoder(): Encoder<SolistingState
|
|||||||
["acceptedResolvers", getArrayEncoder(getAddressEncoder())],
|
["acceptedResolvers", getArrayEncoder(getAddressEncoder())],
|
||||||
["quantity", getU32Encoder()],
|
["quantity", getU32Encoder()],
|
||||||
["quantityReserved", getU32Encoder()],
|
["quantityReserved", getU32Encoder()],
|
||||||
|
["name", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||||
|
["description", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||||
["metadataUri", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
["metadataUri", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||||
["listingId", getU64Encoder()],
|
["listingId", getU64Encoder()],
|
||||||
["isActive", getBooleanEncoder()],
|
["isActive", getBooleanEncoder()],
|
||||||
@@ -174,6 +180,8 @@ export function getSolistingStateListingAccountDecoder(): Decoder<SolistingState
|
|||||||
["acceptedResolvers", getArrayDecoder(getAddressDecoder())],
|
["acceptedResolvers", getArrayDecoder(getAddressDecoder())],
|
||||||
["quantity", getU32Decoder()],
|
["quantity", getU32Decoder()],
|
||||||
["quantityReserved", getU32Decoder()],
|
["quantityReserved", getU32Decoder()],
|
||||||
|
["name", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||||
|
["description", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||||
["metadataUri", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
["metadataUri", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||||
["listingId", getU64Decoder()],
|
["listingId", getU64Decoder()],
|
||||||
["isActive", getBooleanDecoder()],
|
["isActive", getBooleanDecoder()],
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
getBytesEncoder,
|
getBytesEncoder,
|
||||||
getI64Decoder,
|
getI64Decoder,
|
||||||
getI64Encoder,
|
getI64Encoder,
|
||||||
|
getOptionDecoder,
|
||||||
|
getOptionEncoder,
|
||||||
getStructDecoder,
|
getStructDecoder,
|
||||||
getStructEncoder,
|
getStructEncoder,
|
||||||
getU64Decoder,
|
getU64Decoder,
|
||||||
@@ -38,6 +40,8 @@ import {
|
|||||||
type FetchAccountsConfig,
|
type FetchAccountsConfig,
|
||||||
type MaybeAccount,
|
type MaybeAccount,
|
||||||
type MaybeEncodedAccount,
|
type MaybeEncodedAccount,
|
||||||
|
type Option,
|
||||||
|
type OptionOrNullable,
|
||||||
type ReadonlyUint8Array,
|
type ReadonlyUint8Array,
|
||||||
} from "@solana/kit";
|
} from "@solana/kit";
|
||||||
import {
|
import {
|
||||||
@@ -61,7 +65,7 @@ export type SolistingStateOrderAccount = {
|
|||||||
listing: Address;
|
listing: Address;
|
||||||
buyer: Address;
|
buyer: Address;
|
||||||
seller: Address;
|
seller: Address;
|
||||||
resolver: Address;
|
resolver: Option<Address>;
|
||||||
/** The currency the buyer chose to pay in. */
|
/** The currency the buyer chose to pay in. */
|
||||||
paymentCurrency: SolistingStateCurrency;
|
paymentCurrency: SolistingStateCurrency;
|
||||||
/** Actual amount paid (may differ from price when oracle-converted). */
|
/** Actual amount paid (may differ from price when oracle-converted). */
|
||||||
@@ -79,7 +83,7 @@ export type SolistingStateOrderAccountArgs = {
|
|||||||
listing: Address;
|
listing: Address;
|
||||||
buyer: Address;
|
buyer: Address;
|
||||||
seller: Address;
|
seller: Address;
|
||||||
resolver: Address;
|
resolver: OptionOrNullable<Address>;
|
||||||
/** The currency the buyer chose to pay in. */
|
/** The currency the buyer chose to pay in. */
|
||||||
paymentCurrency: SolistingStateCurrencyArgs;
|
paymentCurrency: SolistingStateCurrencyArgs;
|
||||||
/** Actual amount paid (may differ from price when oracle-converted). */
|
/** Actual amount paid (may differ from price when oracle-converted). */
|
||||||
@@ -101,7 +105,7 @@ export function getSolistingStateOrderAccountEncoder(): Encoder<SolistingStateOr
|
|||||||
["listing", getAddressEncoder()],
|
["listing", getAddressEncoder()],
|
||||||
["buyer", getAddressEncoder()],
|
["buyer", getAddressEncoder()],
|
||||||
["seller", getAddressEncoder()],
|
["seller", getAddressEncoder()],
|
||||||
["resolver", getAddressEncoder()],
|
["resolver", getOptionEncoder(getAddressEncoder())],
|
||||||
["paymentCurrency", getSolistingStateCurrencyEncoder()],
|
["paymentCurrency", getSolistingStateCurrencyEncoder()],
|
||||||
["amount", getU64Encoder()],
|
["amount", getU64Encoder()],
|
||||||
["escrowAccount", getAddressEncoder()],
|
["escrowAccount", getAddressEncoder()],
|
||||||
@@ -124,7 +128,7 @@ export function getSolistingStateOrderAccountDecoder(): Decoder<SolistingStateOr
|
|||||||
["listing", getAddressDecoder()],
|
["listing", getAddressDecoder()],
|
||||||
["buyer", getAddressDecoder()],
|
["buyer", getAddressDecoder()],
|
||||||
["seller", getAddressDecoder()],
|
["seller", getAddressDecoder()],
|
||||||
["resolver", getAddressDecoder()],
|
["resolver", getOptionDecoder(getAddressDecoder())],
|
||||||
["paymentCurrency", getSolistingStateCurrencyDecoder()],
|
["paymentCurrency", getSolistingStateCurrencyDecoder()],
|
||||||
["amount", getU64Decoder()],
|
["amount", getU64Decoder()],
|
||||||
["escrowAccount", getAddressDecoder()],
|
["escrowAccount", getAddressDecoder()],
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ export type CreateListingInstructionData = {
|
|||||||
altCurrencies: Array<SolistingStateAltCurrencyConfig>;
|
altCurrencies: Array<SolistingStateAltCurrencyConfig>;
|
||||||
acceptedResolvers: Array<Address>;
|
acceptedResolvers: Array<Address>;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
metadataUri: string;
|
metadataUri: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -122,6 +124,8 @@ export type CreateListingInstructionDataArgs = {
|
|||||||
altCurrencies: Array<SolistingStateAltCurrencyConfigArgs>;
|
altCurrencies: Array<SolistingStateAltCurrencyConfigArgs>;
|
||||||
acceptedResolvers: Array<Address>;
|
acceptedResolvers: Array<Address>;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
metadataUri: string;
|
metadataUri: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,6 +143,8 @@ export function getCreateListingInstructionDataEncoder(): Encoder<CreateListingI
|
|||||||
],
|
],
|
||||||
["acceptedResolvers", getArrayEncoder(getAddressEncoder())],
|
["acceptedResolvers", getArrayEncoder(getAddressEncoder())],
|
||||||
["quantity", getU32Encoder()],
|
["quantity", getU32Encoder()],
|
||||||
|
["name", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||||
|
["description", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||||
["metadataUri", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
["metadataUri", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||||
]),
|
]),
|
||||||
(value) => ({ ...value, discriminator: CREATE_LISTING_DISCRIMINATOR }),
|
(value) => ({ ...value, discriminator: CREATE_LISTING_DISCRIMINATOR }),
|
||||||
@@ -158,6 +164,8 @@ export function getCreateListingInstructionDataDecoder(): Decoder<CreateListingI
|
|||||||
],
|
],
|
||||||
["acceptedResolvers", getArrayDecoder(getAddressDecoder())],
|
["acceptedResolvers", getArrayDecoder(getAddressDecoder())],
|
||||||
["quantity", getU32Decoder()],
|
["quantity", getU32Decoder()],
|
||||||
|
["name", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||||
|
["description", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||||
["metadataUri", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
["metadataUri", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -187,6 +195,8 @@ export type CreateListingAsyncInput<
|
|||||||
altCurrencies: CreateListingInstructionDataArgs["altCurrencies"];
|
altCurrencies: CreateListingInstructionDataArgs["altCurrencies"];
|
||||||
acceptedResolvers: CreateListingInstructionDataArgs["acceptedResolvers"];
|
acceptedResolvers: CreateListingInstructionDataArgs["acceptedResolvers"];
|
||||||
quantity: CreateListingInstructionDataArgs["quantity"];
|
quantity: CreateListingInstructionDataArgs["quantity"];
|
||||||
|
name: CreateListingInstructionDataArgs["name"];
|
||||||
|
description: CreateListingInstructionDataArgs["description"];
|
||||||
metadataUri: CreateListingInstructionDataArgs["metadataUri"];
|
metadataUri: CreateListingInstructionDataArgs["metadataUri"];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -279,6 +289,8 @@ export type CreateListingInput<
|
|||||||
altCurrencies: CreateListingInstructionDataArgs["altCurrencies"];
|
altCurrencies: CreateListingInstructionDataArgs["altCurrencies"];
|
||||||
acceptedResolvers: CreateListingInstructionDataArgs["acceptedResolvers"];
|
acceptedResolvers: CreateListingInstructionDataArgs["acceptedResolvers"];
|
||||||
quantity: CreateListingInstructionDataArgs["quantity"];
|
quantity: CreateListingInstructionDataArgs["quantity"];
|
||||||
|
name: CreateListingInstructionDataArgs["name"];
|
||||||
|
description: CreateListingInstructionDataArgs["description"];
|
||||||
metadataUri: CreateListingInstructionDataArgs["metadataUri"];
|
metadataUri: CreateListingInstructionDataArgs["metadataUri"];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import {
|
|||||||
getAddressEncoder,
|
getAddressEncoder,
|
||||||
getBytesDecoder,
|
getBytesDecoder,
|
||||||
getBytesEncoder,
|
getBytesEncoder,
|
||||||
|
getOptionDecoder,
|
||||||
|
getOptionEncoder,
|
||||||
getProgramDerivedAddress,
|
getProgramDerivedAddress,
|
||||||
getStructDecoder,
|
getStructDecoder,
|
||||||
getStructEncoder,
|
getStructEncoder,
|
||||||
@@ -33,6 +35,8 @@ import {
|
|||||||
type Instruction,
|
type Instruction,
|
||||||
type InstructionWithAccounts,
|
type InstructionWithAccounts,
|
||||||
type InstructionWithData,
|
type InstructionWithData,
|
||||||
|
type Option,
|
||||||
|
type OptionOrNullable,
|
||||||
type ReadonlyAccount,
|
type ReadonlyAccount,
|
||||||
type ReadonlyUint8Array,
|
type ReadonlyUint8Array,
|
||||||
type TransactionSigner,
|
type TransactionSigner,
|
||||||
@@ -121,7 +125,7 @@ export type CreateOrderInstructionData = {
|
|||||||
discriminator: ReadonlyUint8Array;
|
discriminator: ReadonlyUint8Array;
|
||||||
orderId: bigint;
|
orderId: bigint;
|
||||||
escrowId: bigint;
|
escrowId: bigint;
|
||||||
resolver: Address;
|
resolver: Option<Address>;
|
||||||
paymentCurrency: SolistingStateCurrency;
|
paymentCurrency: SolistingStateCurrency;
|
||||||
expectedAmount: bigint;
|
expectedAmount: bigint;
|
||||||
maxSlippageBps: number;
|
maxSlippageBps: number;
|
||||||
@@ -130,7 +134,7 @@ export type CreateOrderInstructionData = {
|
|||||||
export type CreateOrderInstructionDataArgs = {
|
export type CreateOrderInstructionDataArgs = {
|
||||||
orderId: number | bigint;
|
orderId: number | bigint;
|
||||||
escrowId: number | bigint;
|
escrowId: number | bigint;
|
||||||
resolver: Address;
|
resolver: OptionOrNullable<Address>;
|
||||||
paymentCurrency: SolistingStateCurrencyArgs;
|
paymentCurrency: SolistingStateCurrencyArgs;
|
||||||
expectedAmount: number | bigint;
|
expectedAmount: number | bigint;
|
||||||
maxSlippageBps: number;
|
maxSlippageBps: number;
|
||||||
@@ -142,7 +146,7 @@ export function getCreateOrderInstructionDataEncoder(): Encoder<CreateOrderInstr
|
|||||||
["discriminator", fixEncoderSize(getBytesEncoder(), 8)],
|
["discriminator", fixEncoderSize(getBytesEncoder(), 8)],
|
||||||
["orderId", getU64Encoder()],
|
["orderId", getU64Encoder()],
|
||||||
["escrowId", getU64Encoder()],
|
["escrowId", getU64Encoder()],
|
||||||
["resolver", getAddressEncoder()],
|
["resolver", getOptionEncoder(getAddressEncoder())],
|
||||||
["paymentCurrency", getSolistingStateCurrencyEncoder()],
|
["paymentCurrency", getSolistingStateCurrencyEncoder()],
|
||||||
["expectedAmount", getU64Encoder()],
|
["expectedAmount", getU64Encoder()],
|
||||||
["maxSlippageBps", getU16Encoder()],
|
["maxSlippageBps", getU16Encoder()],
|
||||||
@@ -156,7 +160,7 @@ export function getCreateOrderInstructionDataDecoder(): Decoder<CreateOrderInstr
|
|||||||
["discriminator", fixDecoderSize(getBytesDecoder(), 8)],
|
["discriminator", fixDecoderSize(getBytesDecoder(), 8)],
|
||||||
["orderId", getU64Decoder()],
|
["orderId", getU64Decoder()],
|
||||||
["escrowId", getU64Decoder()],
|
["escrowId", getU64Decoder()],
|
||||||
["resolver", getAddressDecoder()],
|
["resolver", getOptionDecoder(getAddressDecoder())],
|
||||||
["paymentCurrency", getSolistingStateCurrencyDecoder()],
|
["paymentCurrency", getSolistingStateCurrencyDecoder()],
|
||||||
["expectedAmount", getU64Decoder()],
|
["expectedAmount", getU64Decoder()],
|
||||||
["maxSlippageBps", getU16Decoder()],
|
["maxSlippageBps", getU16Decoder()],
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ export type UpdateListingInstructionData = {
|
|||||||
altCurrencies: Array<SolistingStateAltCurrencyConfig>;
|
altCurrencies: Array<SolistingStateAltCurrencyConfig>;
|
||||||
acceptedResolvers: Array<Address>;
|
acceptedResolvers: Array<Address>;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
metadataUri: string;
|
metadataUri: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,6 +113,8 @@ export type UpdateListingInstructionDataArgs = {
|
|||||||
altCurrencies: Array<SolistingStateAltCurrencyConfigArgs>;
|
altCurrencies: Array<SolistingStateAltCurrencyConfigArgs>;
|
||||||
acceptedResolvers: Array<Address>;
|
acceptedResolvers: Array<Address>;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
metadataUri: string;
|
metadataUri: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -127,6 +131,8 @@ export function getUpdateListingInstructionDataEncoder(): Encoder<UpdateListingI
|
|||||||
],
|
],
|
||||||
["acceptedResolvers", getArrayEncoder(getAddressEncoder())],
|
["acceptedResolvers", getArrayEncoder(getAddressEncoder())],
|
||||||
["quantity", getU32Encoder()],
|
["quantity", getU32Encoder()],
|
||||||
|
["name", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||||
|
["description", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||||
["metadataUri", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
["metadataUri", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||||
]),
|
]),
|
||||||
(value) => ({ ...value, discriminator: UPDATE_LISTING_DISCRIMINATOR }),
|
(value) => ({ ...value, discriminator: UPDATE_LISTING_DISCRIMINATOR }),
|
||||||
@@ -145,6 +151,8 @@ export function getUpdateListingInstructionDataDecoder(): Decoder<UpdateListingI
|
|||||||
],
|
],
|
||||||
["acceptedResolvers", getArrayDecoder(getAddressDecoder())],
|
["acceptedResolvers", getArrayDecoder(getAddressDecoder())],
|
||||||
["quantity", getU32Decoder()],
|
["quantity", getU32Decoder()],
|
||||||
|
["name", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||||
|
["description", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||||
["metadataUri", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
["metadataUri", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -171,6 +179,8 @@ export type UpdateListingInput<
|
|||||||
altCurrencies: UpdateListingInstructionDataArgs["altCurrencies"];
|
altCurrencies: UpdateListingInstructionDataArgs["altCurrencies"];
|
||||||
acceptedResolvers: UpdateListingInstructionDataArgs["acceptedResolvers"];
|
acceptedResolvers: UpdateListingInstructionDataArgs["acceptedResolvers"];
|
||||||
quantity: UpdateListingInstructionDataArgs["quantity"];
|
quantity: UpdateListingInstructionDataArgs["quantity"];
|
||||||
|
name: UpdateListingInstructionDataArgs["name"];
|
||||||
|
description: UpdateListingInstructionDataArgs["description"];
|
||||||
metadataUri: UpdateListingInstructionDataArgs["metadataUri"];
|
metadataUri: UpdateListingInstructionDataArgs["metadataUri"];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,14 @@
|
|||||||
"name": "quantity",
|
"name": "quantity",
|
||||||
"type": "u32"
|
"type": "u32"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "description",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "metadata_uri",
|
"name": "metadata_uri",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -208,6 +216,14 @@
|
|||||||
"name": "quantity",
|
"name": "quantity",
|
||||||
"type": "u32"
|
"type": "u32"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "description",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "metadata_uri",
|
"name": "metadata_uri",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -1337,6 +1353,14 @@
|
|||||||
],
|
],
|
||||||
"type": "u32"
|
"type": "u32"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "description",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "metadata_uri",
|
"name": "metadata_uri",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export * from './listing'
|
|||||||
// Order exports are imported but we exclude the re-exported deriveEscrowId to avoid duplication
|
// Order exports are imported but we exclude the re-exported deriveEscrowId to avoid duplication
|
||||||
// since it's already exported from pda.js
|
// since it's already exported from pda.js
|
||||||
export type { OrderAccountWithPda } from './order'
|
export type { OrderAccountWithPda } from './order'
|
||||||
export { fetchOrdersForListing, fetchOrdersByBuyer, fetchOrder } from './order'
|
export { fetchOrdersForListing, fetchOrdersByBuyer, fetchOrderByEscrow, fetchOrder } from './order'
|
||||||
|
|
||||||
// Re-export Address for consumers
|
// Re-export Address for consumers
|
||||||
export type { Address, Account } from '@solana/kit'
|
export type { Address, Account } from '@solana/kit'
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { address, type Account, type Address, type Rpc } from '@solana/kit'
|
import { address, type Account, type Address, type Rpc } from '@solana/kit'
|
||||||
import { type GetProgramAccountsApi } from '@solana/rpc-api'
|
import { type GetProgramAccountsApi } from '@solana/rpc-api'
|
||||||
import { type GetAccountInfoApi } from '@solana/rpc-api'
|
import { type GetAccountInfoApi } from '@solana/rpc-api'
|
||||||
import type { Base64EncodedBytes } from '@solana/rpc-types'
|
import type { Base64EncodedBytes, Lamports } from '@solana/rpc-types'
|
||||||
import {
|
import {
|
||||||
decodeSolistingStateOrderAccount,
|
decodeSolistingStateOrderAccount,
|
||||||
fetchSolistingStateOrderAccount,
|
fetchSolistingStateOrderAccount,
|
||||||
SOLISTING_STATE_ORDER_ACCOUNT_DISCRIMINATOR,
|
getSolistingStateOrderAccountDiscriminatorBytes,
|
||||||
type SolistingStateOrderAccount,
|
type SolistingStateOrderAccount,
|
||||||
} from './generated/solisting/src/generated/index'
|
} from './generated/solisting/src/generated/index'
|
||||||
import { deriveEscrowId } from './pda'
|
import { deriveEscrowId } from './pda'
|
||||||
@@ -16,22 +16,57 @@ export type OrderAccountWithPda = Account<SolistingStateOrderAccount>
|
|||||||
type GpaRpc = Rpc<GetProgramAccountsApi>
|
type GpaRpc = Rpc<GetProgramAccountsApi>
|
||||||
type GetRpc = Rpc<GetAccountInfoApi>
|
type GetRpc = Rpc<GetAccountInfoApi>
|
||||||
|
|
||||||
async function fetchAllOrders(rpc: GpaRpc): Promise<OrderAccountWithPda[]> {
|
// OrderAccount byte layout (Sol-currency variant, the only one currently implemented):
|
||||||
const disc = SOLISTING_STATE_ORDER_ACCOUNT_DISCRIMINATOR
|
// 0: discriminator (8)
|
||||||
const discBase64 = Buffer.from(disc).toString('base64') as Base64EncodedBytes
|
// 8: listing (32)
|
||||||
|
// 40: buyer (32)
|
||||||
|
// 72: seller (32)
|
||||||
|
// 104: resolver Option<Pubkey> = 1 byte tag + 32 bytes = 33 bytes
|
||||||
|
// 137: paymentCurrency — Sol variant = 1 byte tag + 0 data = 1 byte total
|
||||||
|
// 138: amount (8)
|
||||||
|
// 146: escrowAccount (32)
|
||||||
|
const LISTING_OFFSET = 8n
|
||||||
|
const BUYER_OFFSET = 40n
|
||||||
|
// escrowAccount offset is only valid for Sol-currency orders.
|
||||||
|
// SPL orders (currently rejected by the program with SplNotImplemented) would sit at offset 179.
|
||||||
|
const ESCROW_ACCOUNT_OFFSET = 146n
|
||||||
|
|
||||||
|
type RawGpaResult = Array<{
|
||||||
|
pubkey: Address
|
||||||
|
account: {
|
||||||
|
executable: boolean
|
||||||
|
lamports: bigint
|
||||||
|
owner: Address
|
||||||
|
space: bigint
|
||||||
|
data: [string, 'base64']
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
|
||||||
|
const DISC_BASE64 = Buffer.from(
|
||||||
|
getSolistingStateOrderAccountDiscriminatorBytes(),
|
||||||
|
).toString('base64') as Base64EncodedBytes
|
||||||
|
|
||||||
|
async function gpa(
|
||||||
|
rpc: GpaRpc,
|
||||||
|
addressFilter: { offset: bigint; addr: Address },
|
||||||
|
): Promise<OrderAccountWithPda[]> {
|
||||||
const results = await rpc
|
const results = await rpc
|
||||||
.getProgramAccounts(PROGRAM_ADDRESS, {
|
.getProgramAccounts(PROGRAM_ADDRESS, {
|
||||||
encoding: 'base64',
|
encoding: 'base64',
|
||||||
filters: [{ memcmp: { offset: 0n, bytes: discBase64, encoding: 'base64' } }],
|
filters: [
|
||||||
|
{ memcmp: { offset: 0n, bytes: DISC_BASE64, encoding: 'base64' } },
|
||||||
|
{ memcmp: { offset: addressFilter.offset, bytes: addressFilter.addr as never, encoding: 'base58' } },
|
||||||
|
],
|
||||||
})
|
})
|
||||||
.send()
|
.send()
|
||||||
return (results as Array<{ pubkey: Address; account: { executable: boolean; lamports: bigint; owner: Address; space: bigint; data: [string, 'base64'] } }>).map((r) => {
|
|
||||||
|
return (results as RawGpaResult).map((r) => {
|
||||||
const data = new Uint8Array(Buffer.from(r.account.data[0], 'base64'))
|
const data = new Uint8Array(Buffer.from(r.account.data[0], 'base64'))
|
||||||
return decodeSolistingStateOrderAccount({
|
return decodeSolistingStateOrderAccount({
|
||||||
address: r.pubkey,
|
address: r.pubkey,
|
||||||
data,
|
data,
|
||||||
executable: r.account.executable,
|
executable: r.account.executable,
|
||||||
lamports: r.account.lamports as unknown as import('@solana/rpc-types').Lamports,
|
lamports: r.account.lamports as unknown as Lamports,
|
||||||
programAddress: r.account.owner,
|
programAddress: r.account.owner,
|
||||||
space: r.account.space,
|
space: r.account.space,
|
||||||
exists: true,
|
exists: true,
|
||||||
@@ -39,26 +74,20 @@ async function fetchAllOrders(rpc: GpaRpc): Promise<OrderAccountWithPda[]> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchOrdersForListing(
|
export function fetchOrdersForListing(rpc: GpaRpc, listingPk: Address): Promise<OrderAccountWithPda[]> {
|
||||||
rpc: GpaRpc,
|
return gpa(rpc, { offset: LISTING_OFFSET, addr: listingPk })
|
||||||
listingPk: Address,
|
|
||||||
): Promise<OrderAccountWithPda[]> {
|
|
||||||
const all = await fetchAllOrders(rpc)
|
|
||||||
return all.filter((o) => o.data.listing === listingPk)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchOrdersByBuyer(
|
export function fetchOrdersByBuyer(rpc: GpaRpc, buyer: Address): Promise<OrderAccountWithPda[]> {
|
||||||
rpc: GpaRpc,
|
return gpa(rpc, { offset: BUYER_OFFSET, addr: buyer })
|
||||||
buyer: Address,
|
|
||||||
): Promise<OrderAccountWithPda[]> {
|
|
||||||
const all = await fetchAllOrders(rpc)
|
|
||||||
return all.filter((o) => o.data.buyer === buyer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchOrder(
|
export async function fetchOrderByEscrow(rpc: GpaRpc, escrowPda: Address): Promise<OrderAccountWithPda | null> {
|
||||||
rpc: GetRpc,
|
const results = await gpa(rpc, { offset: ESCROW_ACCOUNT_OFFSET, addr: escrowPda })
|
||||||
addr: Address,
|
return results[0] ?? null
|
||||||
): Promise<OrderAccountWithPda | null> {
|
}
|
||||||
|
|
||||||
|
export async function fetchOrder(rpc: GetRpc, addr: Address): Promise<OrderAccountWithPda | null> {
|
||||||
return fetchSolistingStateOrderAccount(rpc, addr).catch(() => null)
|
return fetchSolistingStateOrderAccount(rpc, addr).catch(() => null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user