Compare commits

...

10 Commits

Author SHA1 Message Date
thesn10
8920463fdd readme md 2026-07-08 16:25:20 +02:00
thesn10
e5a0e74ff2 upgrade deps 2026-06-26 17:32:43 +02:00
thesn10
e76f5dc565 update litesvm 2026-06-22 22:25:49 +02:00
thesn10
1dd85b5b9c fix ts errors 2026-05-26 21:46:20 +02:00
thesn10
5da7fb2b97 fix errors 2026-05-26 21:45:35 +02:00
thesn10
f97792f190 update sdk 2026-05-26 21:37:25 +02:00
thesn10
9d9c7a8a43 buyer initiated flow 2026-05-26 21:33:32 +02:00
thesn10
dd5ec25ff9 rename canocial amount to price 2026-05-25 21:55:02 +02:00
thesn10
9900ada281 better oracle 2026-05-25 21:40:09 +02:00
thesn10
0b00030167 use correct sdk 2026-05-25 21:24:46 +02:00
51 changed files with 4672 additions and 1909 deletions

View File

@@ -1 +1,6 @@
approvedGitRepositories:
- "**"
enableScripts: true
nodeLinker: node-modules nodeLinker: node-modules

97
Cargo.lock generated
View File

@@ -1076,7 +1076,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anchor-lang", "anchor-lang",
"descro_ext_resolvers", "descro_ext_resolvers",
"litesvm", "litesvm 0.12.0",
"solana-keypair", "solana-keypair",
"solana-message", "solana-message",
"solana-signer", "solana-signer",
@@ -1088,7 +1088,7 @@ name = "descro_ext_resolvers"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anchor-lang", "anchor-lang",
"litesvm", "litesvm 0.10.0",
"solana-keypair", "solana-keypair",
"solana-message", "solana-message",
"solana-signer", "solana-signer",
@@ -1686,7 +1686,7 @@ dependencies = [
"solana-precompile-error", "solana-precompile-error",
"solana-program-error", "solana-program-error",
"solana-program-runtime", "solana-program-runtime",
"solana-rent", "solana-rent 3.1.0",
"solana-sdk-ids", "solana-sdk-ids",
"solana-sha256-hasher", "solana-sha256-hasher",
"solana-signature", "solana-signature",
@@ -1708,6 +1708,70 @@ dependencies = [
"thiserror 2.0.18", "thiserror 2.0.18",
] ]
[[package]]
name = "litesvm"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "236efc1fc49db2af581f221faea4bbc1088dd72d9aa381461eeefef14d48ea1b"
dependencies = [
"agave-feature-set",
"agave-reserved-account-keys",
"agave-syscalls",
"ansi_term",
"bincode",
"indexmap",
"itertools 0.14.0",
"log",
"serde",
"solana-account",
"solana-address 2.6.0",
"solana-address-lookup-table-interface",
"solana-bpf-loader-program",
"solana-builtins",
"solana-clock",
"solana-compute-budget",
"solana-compute-budget-instruction",
"solana-epoch-rewards",
"solana-epoch-schedule",
"solana-feature-gate-interface",
"solana-fee",
"solana-fee-structure",
"solana-hash 3.1.0",
"solana-instruction",
"solana-instructions-sysvar",
"solana-keypair",
"solana-last-restart-slot",
"solana-loader-v3-interface",
"solana-loader-v4-interface",
"solana-message",
"solana-native-token",
"solana-nonce",
"solana-nonce-account",
"solana-precompile-error",
"solana-program-error",
"solana-program-runtime",
"solana-rent 3.1.0",
"solana-sdk-ids",
"solana-sha256-hasher",
"solana-signature",
"solana-signer",
"solana-slot-hashes",
"solana-slot-history",
"solana-stake-interface",
"solana-svm-callback",
"solana-svm-log-collector",
"solana-svm-timings",
"solana-svm-transaction",
"solana-system-interface 3.2.0",
"solana-system-program",
"solana-sysvar",
"solana-sysvar-id",
"solana-transaction",
"solana-transaction-context",
"solana-transaction-error",
"thiserror 2.0.18",
]
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.14" version = "0.4.14"
@@ -2721,9 +2785,17 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75ca9b5cbb6f500f7fd73db5bd95640f71a83f04d6121a0e59a43b202dca2731" checksum = "75ca9b5cbb6f500f7fd73db5bd95640f71a83f04d6121a0e59a43b202dca2731"
dependencies = [ dependencies = [
"bincode",
"serde",
"serde_derive",
"solana-account",
"solana-account-info",
"solana-instruction",
"solana-program-error", "solana-program-error",
"solana-pubkey 4.2.0", "solana-pubkey 4.2.0",
"solana-rent 4.2.0",
"solana-sdk-ids", "solana-sdk-ids",
"solana-system-interface 3.2.0",
] ]
[[package]] [[package]]
@@ -3094,7 +3166,7 @@ dependencies = [
"solana-loader-v3-interface", "solana-loader-v3-interface",
"solana-program-entrypoint", "solana-program-entrypoint",
"solana-pubkey 3.0.0", "solana-pubkey 3.0.0",
"solana-rent", "solana-rent 3.1.0",
"solana-sbpf", "solana-sbpf",
"solana-sdk-ids", "solana-sdk-ids",
"solana-slot-hashes", "solana-slot-hashes",
@@ -3145,6 +3217,15 @@ dependencies = [
"solana-sysvar-id", "solana-sysvar-id",
] ]
[[package]]
name = "solana-rent"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9809b081e99bc142ce803bcd7ee18306759ce3b30a96a9da3f6f41c45e50ef0"
dependencies = [
"solana-sdk-macro",
]
[[package]] [[package]]
name = "solana-sanitize" name = "solana-sanitize"
version = "3.0.1" version = "3.0.1"
@@ -3488,7 +3569,7 @@ dependencies = [
"solana-program-error", "solana-program-error",
"solana-program-memory", "solana-program-memory",
"solana-pubkey 4.2.0", "solana-pubkey 4.2.0",
"solana-rent", "solana-rent 3.1.0",
"solana-sdk-ids", "solana-sdk-ids",
"solana-sdk-macro", "solana-sdk-macro",
"solana-slot-hashes", "solana-slot-hashes",
@@ -3540,7 +3621,7 @@ dependencies = [
"solana-instruction", "solana-instruction",
"solana-instructions-sysvar", "solana-instructions-sysvar",
"solana-pubkey 3.0.0", "solana-pubkey 3.0.0",
"solana-rent", "solana-rent 3.1.0",
"solana-sbpf", "solana-sbpf",
"solana-sdk-ids", "solana-sdk-ids",
] ]
@@ -3575,7 +3656,7 @@ dependencies = [
"solana-instruction", "solana-instruction",
"solana-instruction-error", "solana-instruction-error",
"solana-pubkey 3.0.0", "solana-pubkey 3.0.0",
"solana-rent", "solana-rent 3.1.0",
"solana-sdk-ids", "solana-sdk-ids",
"solana-serde-varint", "solana-serde-varint",
"solana-serialize-utils", "solana-serialize-utils",
@@ -3605,7 +3686,7 @@ dependencies = [
"solana-packet", "solana-packet",
"solana-program-runtime", "solana-program-runtime",
"solana-pubkey 3.0.0", "solana-pubkey 3.0.0",
"solana-rent", "solana-rent 3.1.0",
"solana-sdk-ids", "solana-sdk-ids",
"solana-signer", "solana-signer",
"solana-slot-hashes", "solana-slot-hashes",

View File

@@ -2,6 +2,20 @@
A decentralized escrow protocol on Solana for physical goods trading. Fully permissionless and resolver-agnostic — any wallet or program that implements the resolver interface can adjudicate disputes. A decentralized escrow protocol on Solana for physical goods trading. Fully permissionless and resolver-agnostic — any wallet or program that implements the resolver interface can adjudicate disputes.
## 🌍 Vision & Concept
Buying and selling physical goods peer-to-peer runs on trust — and today that trust is either placed in a centralized platform that takes a cut and can freeze your funds, or in nothing at all, leaving buyers and sellers to hope the other side follows through. descro removes that single point of control: funds sit in an on-chain vault that only releases according to rules enforced by the protocol itself, not by a company.
Instead of building one monolithic marketplace, descro splits the problem into a minimal **protocol layer** (escrow logic + a resolver registry) and lets anyone build the **application layer** on top — from a slick, KYC'd consumer app to a raw wallet-to-wallet trade between two people who've never met.
- ⚖️ **Resolver-agnostic by design** — disputes aren't adjudicated by descro itself. Any wallet or program that implements the resolver interface can be plugged in: a single trusted authority, a multisig, a jury DAO, or an algorithmic (MAD-style) resolver.
- 🔓 **Fully permissionless** — no whitelist, no gatekeeping. Anyone can create an escrow, register as a resolver, or build a client against the protocol.
- 🔒 **Non-custodial by construction** — SOL lives in a program-owned vault, not a company's balance sheet. Nobody, including descro itself, can move funds without satisfying the on-chain rules.
- 🧩 **Progressive decentralization** — a central platform (own resolver, KYC, familiar UX) can onboard mainstream users first, while the same trades quietly build up a decentralized backend anyone can tap into directly, with free choice of resolver, once critical mass is reached.
-**Built for Solana** — sub-cent transaction fees and ~400ms finality mean the on-chain plumbing can stay invisible to end users while still being fully verifiable.
- 🧱 **Minimal, composable core** — the escrow program only knows about escrow state and SOL; dispute resolution is entirely external via CPI, so the protocol doesn't need to be trusted to "get justice right," only to enforce the interface correctly.
- 🛡️ **Trust-minimized stats** — resolver reputation (fee, resolution history) lives in an on-chain registry that only the escrow program can update, so it can't be gamed by a resolver inflating its own track record.
## Overview ## Overview
``` ```

View File

@@ -10,19 +10,20 @@
"@descro/sdk": "workspace:*", "@descro/sdk": "workspace:*",
"@solana/connector": "^0.2.4", "@solana/connector": "^0.2.4",
"@solana/connector-debugger": "^0.1.1", "@solana/connector-debugger": "^0.1.1",
"@solana/kit": "^6.0.0", "@solana/kit": "^6.10.0",
"@tamagui/config": "^2.0.0-rc.42", "@solana/web3.js": "^1.98.4",
"@tamagui/next-theme": "^2.0.0-rc.42", "@tamagui/config": "^2.3.3",
"next": "^16.2.6", "@tamagui/next-theme": "^2.3.3",
"react": "^19.2.6", "next": "^16.2.9",
"react-dom": "^19.2.6", "react": "^19.2.7",
"react-dom": "^19.2.7",
"react-native-web": "^0.21.2", "react-native-web": "^0.21.2",
"tamagui": "^2.0.0-rc.42" "tamagui": "^2.3.3"
}, },
"devDependencies": { "devDependencies": {
"@tamagui/cli": "^2.0.0-rc.42", "@tamagui/cli": "^2.3.3",
"@types/node": "^25.9.0", "@types/node": "^26.0.1",
"@types/react": "^19.2.14", "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"typescript": "^6.0.3" "typescript": "^6.0.3"
} }

View File

@@ -14,11 +14,13 @@ import {
getSignatureFromTransaction, getSignatureFromTransaction,
address as kitAddress, address as kitAddress,
} from '@solana/kit' } from '@solana/kit'
import { useKitTransactionSigner, useWallet, useCluster, useSolanaClient } from '@solana/connector/react' import { useKitTransactionSigner, useWallet, useSolanaClient } from '@solana/connector/react'
import { getCreateEscrowInstructionAsync } from '@descro/sdk' import { getCreateEscrowInstructionAsync, getBuyerCreateEscrowInstructionAsync, findResolverEntryPda } from '@descro/sdk'
import { lamportsFromSol } from '@/util/lamports' import { lamportsFromSol } from '@/util/lamports'
import type { Address } from '@descro/sdk' import type { Address } from '@descro/sdk'
const SYSTEM_PROGRAM_ADDRESS = '11111111111111111111111111111111' as Address
function getNextEscrowId(walletAddress: string): bigint { function getNextEscrowId(walletAddress: string): bigint {
const key = `descro_escrow_id_${walletAddress}` const key = `descro_escrow_id_${walletAddress}`
const stored = localStorage.getItem(key) const stored = localStorage.getItem(key)
@@ -28,6 +30,15 @@ function getNextEscrowId(walletAddress: string): bigint {
return BigInt(next) return BigInt(next)
} }
function getNextBuyerEscrowId(buyerAddress: string, sellerAddress: string): bigint {
const key = `descro_buyer_escrow_id_${buyerAddress}_${sellerAddress}`
const stored = localStorage.getItem(key)
const current = stored ? parseInt(stored, 10) : 0
const next = current + 1
localStorage.setItem(key, next.toString())
return BigInt(next)
}
interface CreateEscrowFormProps { interface CreateEscrowFormProps {
onCreated?: () => void onCreated?: () => void
} }
@@ -37,7 +48,8 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
const { account: address } = useWallet() const { account: address } = useWallet()
const { client } = useSolanaClient() const { client } = useSolanaClient()
const [buyer, setBuyer] = useState('') const [mode, setMode] = useState<'seller' | 'buyer'>('seller')
const [counterparty, setCounterparty] = useState('')
const [amountSol, setAmountSol] = useState('') const [amountSol, setAmountSol] = useState('')
const [resolver, setResolver] = useState('') const [resolver, setResolver] = useState('')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@@ -54,11 +66,11 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
return return
} }
let buyerAddress: Address let counterpartyAddress: Address
try { try {
buyerAddress = kitAddress(buyer) counterpartyAddress = kitAddress(counterparty)
} catch { } catch {
setError('Invalid buyer public key.') setError(`Invalid ${mode === 'seller' ? 'buyer' : 'seller'} public key.`)
return return
} }
@@ -68,7 +80,6 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
return return
} }
const amountLamports = lamportsFromSol(amountSolNum) const amountLamports = lamportsFromSol(amountSolNum)
// Vault is a 0-byte PDA funded entirely by the deposit; must be rent-exempt (~890,880 lamports)
if (amountLamports < 890_880n) { if (amountLamports < 890_880n) {
setError('Amount too small: minimum is ~0.00089 SOL (vault must be rent-exempt).') setError('Amount too small: minimum is ~0.00089 SOL (vault must be rent-exempt).')
return return
@@ -87,15 +98,37 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
setLoading(true) setLoading(true)
try { try {
const { rpc, rpcSubscriptions } = client const { rpc, rpcSubscriptions } = client
const escrowId = getNextEscrowId(address)
const ix = await getCreateEscrowInstructionAsync({ let ix: unknown
seller: signer, let escrowIdLabel: bigint
buyer: buyerAddress,
amount: amountLamports, if (mode === 'seller') {
disputeResolver: resolverAddress, const escrowId = getNextEscrowId(address)
escrowId, escrowIdLabel = escrowId
}) const resolverAcct = resolverAddress ?? SYSTEM_PROGRAM_ADDRESS
const [resolverEntryAcct] = resolverAddress
? await findResolverEntryPda({ authority: resolverAddress })
: [SYSTEM_PROGRAM_ADDRESS]
ix = await getCreateEscrowInstructionAsync({
seller: signer,
buyer: counterpartyAddress,
resolver: resolverAcct,
resolverEntry: resolverEntryAcct,
amount: amountLamports,
disputeResolver: resolverAddress,
escrowId,
})
} else {
const escrowId = getNextBuyerEscrowId(address, counterpartyAddress)
escrowIdLabel = escrowId
ix = await getBuyerCreateEscrowInstructionAsync({
buyer: signer,
seller: counterpartyAddress,
amount: amountLamports,
disputeResolver: resolverAddress,
escrowId,
})
}
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send() const { value: latestBlockhash } = await rpc.getLatestBlockhash().send()
@@ -108,11 +141,11 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
const signed = await signTransactionMessageWithSigners(txMsg) const signed = await signTransactionMessageWithSigners(txMsg)
assertIsTransactionWithBlockhashLifetime(signed) assertIsTransactionWithBlockhashLifetime(signed)
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signed, { commitment: 'confirmed' }) await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions: rpcSubscriptions as never })(signed, { commitment: 'confirmed' })
const sig = getSignatureFromTransaction(signed) const sig = getSignatureFromTransaction(signed)
setSuccess(`Escrow #${escrowId} created! Tx: ${sig.slice(0, 16)}`) setSuccess(`Escrow #${escrowIdLabel} created! Tx: ${sig.slice(0, 16)}`)
setBuyer('') setCounterparty('')
setAmountSol('') setAmountSol('')
setResolver('') setResolver('')
onCreated?.() onCreated?.()
@@ -123,16 +156,46 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
} }
} }
const counterpartyLabel = mode === 'seller' ? 'Buyer Public Key' : 'Seller Public Key'
const buttonLabel = mode === 'seller' ? 'Create Escrow' : 'Create & Fund Escrow'
return ( return (
<YStack gap="$3"> <YStack gap="$3">
<Text fontSize={16} fontWeight="700">Create Escrow</Text> <Text fontSize={16} fontWeight="700">Create Escrow</Text>
<XStack gap="$2">
<Button
size="$2"
theme={mode === 'seller' ? 'active' : undefined}
onPress={() => { setMode('seller'); setCounterparty(''); setError(null); setSuccess(null) }}
flex={1}
>
I am Seller
</Button>
<Button
size="$2"
theme={mode === 'buyer' ? 'active' : undefined}
onPress={() => { setMode('buyer'); setCounterparty(''); setError(null); setSuccess(null) }}
flex={1}
>
I am Buyer
</Button>
</XStack>
{mode === 'buyer' && (
<XStack backgroundColor="$purple3" padding="$2" borderRadius="$2">
<Text color="$purple9" fontSize={12}>
Buyer flow: you create and fund the escrow immediately. The seller must confirm before the escrow becomes active.
</Text>
</XStack>
)}
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> <form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<YStack gap="$1"> <YStack gap="$1">
<Text fontSize={13} color="$color11">Buyer Public Key *</Text> <Text fontSize={13} color="$color11">{counterpartyLabel} *</Text>
<Input <Input
value={buyer} value={counterparty}
onChangeText={setBuyer} onChangeText={setCounterparty}
placeholder="Pubkey..." placeholder="Pubkey..."
style={{ fontFamily: 'monospace' }} style={{ fontFamily: 'monospace' }}
fontSize={13} fontSize={13}
@@ -178,7 +241,7 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
disabled={loading || !address} disabled={loading || !address}
icon={loading ? <Spinner /> : undefined} icon={loading ? <Spinner /> : undefined}
> >
{loading ? 'Creating…' : 'Create Escrow'} {loading ? 'Creating…' : buttonLabel}
</Button> </Button>
</form> </form>
</YStack> </YStack>

View File

@@ -4,6 +4,7 @@ import React from 'react'
import { YStack, XStack, Text } from 'tamagui' import { YStack, XStack, Text } from 'tamagui'
import { StatusBadge } from './StatusBadge' import { StatusBadge } from './StatusBadge'
import type { EscrowAccountWithPda } from '@descro/sdk' import type { EscrowAccountWithPda } from '@descro/sdk'
import { lamports } from '@solana/kit'
import { lamportsToSolString } from '@/util/lamports' import { lamportsToSolString } from '@/util/lamports'
function truncAddr(addr: string): string { function truncAddr(addr: string): string {
return `${addr.slice(0, 4)}${addr.slice(-4)}` return `${addr.slice(0, 4)}${addr.slice(-4)}`
@@ -17,7 +18,7 @@ interface EscrowCardProps {
export function EscrowCard({ item, selected, onSelect }: EscrowCardProps) { export function EscrowCard({ item, selected, onSelect }: EscrowCardProps) {
const { pda, account } = item const { pda, account } = item
const solAmount = lamportsToSolString(account.amount, 4) const solAmount = lamportsToSolString(lamports(account.amount), 4)
return ( return (
<YStack <YStack

View File

@@ -13,18 +13,21 @@ import {
assertIsTransactionWithBlockhashLifetime, assertIsTransactionWithBlockhashLifetime,
getSignatureFromTransaction, getSignatureFromTransaction,
isSome, isSome,
lamports,
} from '@solana/kit' } from '@solana/kit'
import { useKitTransactionSigner, useWallet, useSolanaClient } from '@solana/connector/react' import { useKitTransactionSigner, useWallet, useSolanaClient } from '@solana/connector/react'
import { StatusBadge } from './StatusBadge' import { StatusBadge } from './StatusBadge'
import { import {
isAwaitingDeposit, isAwaitingDeposit,
isAwaitingSellerConfirm,
isActive, isActive,
isDisputed, isDisputed,
getDepositInstructionAsync, getDepositInstructionAsync,
getCancelInstruction, getCancelInstructionAsync,
getCompleteInstructionAsync, getCompleteInstructionAsync,
getDisputeInstruction, getDisputeInstruction,
getResolveInstructionAsync, getResolveInstructionAsync,
getSellerConfirmInstruction,
findResolverEntryPda, findResolverEntryPda,
DESCRO_EXT_RESOLVERS_PROGRAM_ADDRESS, DESCRO_EXT_RESOLVERS_PROGRAM_ADDRESS,
Winner, Winner,
@@ -51,7 +54,7 @@ export function EscrowDetail({ item, onAction }: EscrowDetailProps) {
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null) const [success, setSuccess] = useState<string | null>(null)
const solAmount = lamportsToSolString(escrow.amount, 4) const solAmount = lamportsToSolString(lamports(escrow.amount), 4)
const isSeller = address != null && address === escrow.seller const isSeller = address != null && address === escrow.seller
const isBuyer = address != null && address === escrow.buyer const isBuyer = address != null && address === escrow.buyer
const resolverAddr = isSome(escrow.disputeResolver) ? escrow.disputeResolver.value : null const resolverAddr = isSome(escrow.disputeResolver) ? escrow.disputeResolver.value : null
@@ -77,7 +80,7 @@ export function EscrowDetail({ item, onAction }: EscrowDetailProps) {
) )
const signed = await signTransactionMessageWithSigners(txMsg) const signed = await signTransactionMessageWithSigners(txMsg)
assertIsTransactionWithBlockhashLifetime(signed) assertIsTransactionWithBlockhashLifetime(signed)
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signed, { commitment: 'confirmed' }) await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions: rpcSubscriptions as never })(signed, { commitment: 'confirmed' })
const sig = getSignatureFromTransaction(signed) const sig = getSignatureFromTransaction(signed)
setSuccess(`Done! Tx: ${sig.slice(0, 20)}`) setSuccess(`Done! Tx: ${sig.slice(0, 20)}`)
onAction?.() onAction?.()
@@ -93,7 +96,22 @@ export function EscrowDetail({ item, onAction }: EscrowDetailProps) {
runTx(() => getDepositInstructionAsync({ buyer: signer!, escrowAccount: pda })) runTx(() => getDepositInstructionAsync({ buyer: signer!, escrowAccount: pda }))
const handleCancel = () => const handleCancel = () =>
runTx(async () => getCancelInstruction({ seller: signer!, escrowAccount: pda })) runTx(() => getCancelInstructionAsync({ canceller: signer!, buyer: escrow.buyer, escrowAccount: pda }))
const handleSellerConfirm = () =>
runTx(async () => {
const SYSTEM_PROGRAM = '11111111111111111111111111111111' as Parameters<typeof getSellerConfirmInstruction>[0]['resolver']
const resolver = resolverAddr ?? SYSTEM_PROGRAM
const [resolverEntryAddr] = resolverAddr
? await findResolverEntryPda({ authority: resolverAddr })
: [SYSTEM_PROGRAM]
return getSellerConfirmInstruction({
seller: signer!,
resolver,
escrowAccount: pda,
resolverEntry: resolverEntryAddr,
})
})
const handleComplete = () => const handleComplete = () =>
runTx(() => runTx(() =>
@@ -152,7 +170,17 @@ export function EscrowDetail({ item, onAction }: EscrowDetailProps) {
Deposit Deposit
</Button> </Button>
)} )}
{isAwaitingDeposit(escrow.state) && isSeller && ( {isAwaitingDeposit(escrow.state) && (isSeller || isBuyer) && (
<Button theme="red" onPress={handleCancel} disabled={loading} icon={loading ? <Spinner /> : undefined}>
Cancel
</Button>
)}
{isAwaitingSellerConfirm(escrow.state) && isSeller && (
<Button theme="green" onPress={handleSellerConfirm} disabled={loading} icon={loading ? <Spinner /> : undefined}>
Confirm Escrow
</Button>
)}
{isAwaitingSellerConfirm(escrow.state) && (isSeller || isBuyer) && (
<Button theme="red" onPress={handleCancel} disabled={loading} icon={loading ? <Spinner /> : undefined}> <Button theme="red" onPress={handleCancel} disabled={loading} icon={loading ? <Spinner /> : undefined}>
Cancel Cancel
</Button> </Button>

View File

@@ -20,6 +20,7 @@ import {
getRegisterResolverInstructionAsync, getRegisterResolverInstructionAsync,
resolverTypeLabel, resolverTypeLabel,
ResolverType, ResolverType,
AcceptancePolicy,
} from '@descro/sdk' } from '@descro/sdk'
import type { ResolverEntryWithPda } from '@descro/sdk' import type { ResolverEntryWithPda } from '@descro/sdk'
@@ -49,6 +50,13 @@ export function ResolverPanel() {
const [feeRecipient, setFeeRecipient] = useState('') const [feeRecipient, setFeeRecipient] = useState('')
const [metadataUri, setMetadataUri] = useState('') const [metadataUri, setMetadataUri] = useState('')
const [resolverTypeIdx, setResolverTypeIdx] = useState(0) const [resolverTypeIdx, setResolverTypeIdx] = useState(0)
const [acceptancePolicyIdx, setAcceptancePolicyIdx] = useState(0)
const ACCEPTANCE_POLICY_OPTIONS: { label: string; value: AcceptancePolicy }[] = [
{ label: 'Open (anyone can assign you)', value: AcceptancePolicy.Open },
{ label: 'SignatureGated (you must co-sign)', value: AcceptancePolicy.SignatureGated },
{ label: 'ProgramGated', value: AcceptancePolicy.ProgramGated },
]
const [formLoading, setFormLoading] = useState(false) const [formLoading, setFormLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
@@ -110,6 +118,7 @@ export function ResolverPanel() {
const ix = await getRegisterResolverInstructionAsync({ const ix = await getRegisterResolverInstructionAsync({
authority: signer, authority: signer,
resolverType, resolverType,
acceptancePolicy: ACCEPTANCE_POLICY_OPTIONS[acceptancePolicyIdx].value,
name: name.trim(), name: name.trim(),
description: description.trim(), description: description.trim(),
feeBps: fee, feeBps: fee,
@@ -126,7 +135,7 @@ export function ResolverPanel() {
) )
const signed = await signTransactionMessageWithSigners(txMsg) const signed = await signTransactionMessageWithSigners(txMsg)
assertIsTransactionWithBlockhashLifetime(signed) assertIsTransactionWithBlockhashLifetime(signed)
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signed, { commitment: 'confirmed' }) await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions: rpcSubscriptions as never })(signed, { commitment: 'confirmed' })
const sig = getSignatureFromTransaction(signed) const sig = getSignatureFromTransaction(signed)
setSuccess(`Registered! Tx: ${sig.slice(0, 20)}`) setSuccess(`Registered! Tx: ${sig.slice(0, 20)}`)
@@ -217,6 +226,19 @@ export function ResolverPanel() {
</select> </select>
</YStack> </YStack>
<YStack gap="$1">
<Text fontSize={13} color="$color11">Acceptance Policy</Text>
<select
value={acceptancePolicyIdx}
onChange={(e) => setAcceptancePolicyIdx(parseInt(e.target.value, 10))}
style={{ padding: '8px', borderRadius: 6, border: '1px solid #ccc', fontSize: 13 }}
>
{ACCEPTANCE_POLICY_OPTIONS.map((opt, i) => (
<option key={i} value={i}>{opt.label}</option>
))}
</select>
</YStack>
<YStack gap="$1"> <YStack gap="$1">
<Text fontSize={13} color="$color11">Fee (BPS)</Text> <Text fontSize={13} color="$color11">Fee (BPS)</Text>
<Input value={feeBps} onChangeText={setFeeBps} placeholder="0" keyboardType="numeric" fontSize={13} /> <Input value={feeBps} onChangeText={setFeeBps} placeholder="0" keyboardType="numeric" fontSize={13} />

View File

@@ -7,6 +7,7 @@ import type { EscrowState } from '@descro/sdk'
const STATE_COLORS: Record<string, string> = { const STATE_COLORS: Record<string, string> = {
AwaitingDeposit: '$blue9', AwaitingDeposit: '$blue9',
AwaitingSellerConfirm: '$purple9',
Active: '$green9', Active: '$green9',
Disputed: '$orange9', Disputed: '$orange9',
Complete: '$gray9', Complete: '$gray9',

View File

@@ -36,7 +36,7 @@ export function useEscrowDetail(pda: Address | null) {
if (!cancelled) setLoading(false) if (!cancelled) setLoading(false)
}) })
const unsub = subscribeEscrow(rpcSubscriptions, pda, (updated) => { const unsub = subscribeEscrow(rpcSubscriptions as never, pda, (updated) => {
if (!cancelled) setAccount(updated) if (!cancelled) setAccount(updated)
}) })

View File

@@ -9,9 +9,10 @@
**Goal:** Build the `solisting` Anchor program — a coordination and discovery layer for product listings and bilateral order consent, orchestrating the full flow from buyer intent → bilateral consent → active `descro` escrow without ever touching the descro vault directly. **Goal:** Build the `solisting` Anchor program — a coordination and discovery layer for product listings and bilateral order consent, orchestrating the full flow from buyer intent → bilateral consent → active `descro` escrow without ever touching the descro vault directly.
**Architecture:** **Architecture:**
- `ListingAccount` holds a single canonical price (`canonical_currency` + `canonical_amount`) as the sole source of truth. Alternative currencies (`alt_currencies`) are always converted from canonical at `create_order` time via an on-chain Pyth oracle — no independent price per currency, no arbitrage possible. The oracle account is only required when `alt_currencies` is non-empty. - `ListingAccount` holds a single canonical price (`canonical_currency` + `price`) as the sole source of truth. Alternative currencies (`alt_currencies: Vec<AltCurrencyConfig>`) are always converted from canonical at `create_order` time via on-chain Pyth oracles — no independent price per currency, no arbitrage possible.
- Each `AltCurrencyConfig` carries its own `usd_oracle: Option<Pubkey>` (the Pyth TOKEN/USD feed for that token; `None` = treat as a $1 USD stablecoin). The canonical currency has a parallel `canonical_oracle: Option<Pubkey>`. `Currency::Spl` stores `decimals: u8` so the program never needs to look up the mint.
- `OrderAccount` records the bilateral consent: which currency the buyer chose, the oracle-computed amount, and a pointer to the descro escrow. - `OrderAccount` records the bilateral consent: which currency the buyer chose, the oracle-computed amount, and a pointer to the descro escrow.
- Four seller archetypes: (1) SOL-only, (2) SOL-primary + USDT via oracle, (3) USDT-only, (4) USDT-primary + SOL via oracle. - The oracle module converts between any two currencies through USD as an intermediate (using up to two Pyth feeds): `canonical_usd = amount × canonical_price`, `target_amount = canonical_usd ÷ target_price`. Both sides can independently be a stablecoin (`None` oracle = $1 peg, no account needed).
- `accepted_resolvers` is a global list per listing — resolvers handle disputes, not payments, so one list covers all currencies. - `accepted_resolvers` is a global list per listing — resolvers handle disputes, not payments, so one list covers all currencies.
- `create_order` CPIs `descro.buyer_create_escrow` atomically. `accept_order` CPIs `descro.seller_confirm`. Solisting never holds or transfers vault funds. - `create_order` CPIs `descro.buyer_create_escrow` atomically. `accept_order` CPIs `descro.seller_confirm`. Solisting never holds or transfers vault funds.
- State drift (buyer cancels directly on descro) is handled gracefully: terminal instructions read descro state first and skip the CPI if the escrow is already resolved. - State drift (buyer cancels directly on descro) is handled gracefully: terminal instructions read descro state first and skip the CPI if the escrow is already resolved.
@@ -25,7 +26,7 @@
- `max_slippage_bps` argument on `create_order` protects buyer from price movement between submission and confirmation - `max_slippage_bps` argument on `create_order` protects buyer from price movement between submission and confirmation
- SPL token path (`descro_spl` CPI) is not yet implemented — `create_order` errors on `payment_currency = Spl` until `descro_spl` exists; state and oracle are designed to support it without schema changes - SPL token path (`descro_spl` CPI) is not yet implemented — `create_order` errors on `payment_currency = Spl` until `descro_spl` exists; state and oracle are designed to support it without schema changes
**Tech Stack:** Rust, Anchor 1.0.x, LiteSVM 0.10.0, `pyth-sdk-solana` for oracle reads. Build order: `descro_ext_resolvers``descro``solisting`. **Tech Stack:** Rust, Anchor 1.0.x, LiteSVM 0.10.0, `pyth-solana-receiver-sdk` for oracle reads. Build order: `descro_ext_resolvers``descro``solisting`.
--- ---
@@ -33,11 +34,11 @@
| Action | Path | Responsibility | | Action | Path | Responsibility |
|---|---|---| |---|---|---|
| Create | `programs/solisting/Cargo.toml` | Crate definition + dependencies (incl. pyth-sdk-solana) | | Create | `programs/solisting/Cargo.toml` | Crate definition + dependencies (incl. pyth-solana-receiver-sdk) |
| Create | `programs/solisting/src/lib.rs` | Program entrypoints + declare_id! | | Create | `programs/solisting/src/lib.rs` | Program entrypoints + declare_id! |
| Create | `programs/solisting/src/state.rs` | `ListingAccount`, `OrderAccount`, `Currency` enum | | Create | `programs/solisting/src/state.rs` | `ListingAccount`, `OrderAccount`, `Currency` enum, `AltCurrencyConfig` |
| Create | `programs/solisting/src/error.rs` | `SolistingError` enum | | Create | `programs/solisting/src/error.rs` | `SolistingError` enum |
| Create | `programs/solisting/src/oracle.rs` | Pyth price read + SOL↔stablecoin conversion + slippage check | | Create | `programs/solisting/src/oracle.rs` | Generic Pyth feed reader + USD-intermediate cross-currency conversion + slippage check |
| Create | `programs/solisting/src/instructions.rs` | Module re-exports | | Create | `programs/solisting/src/instructions.rs` | Module re-exports |
| Create | `programs/solisting/src/instructions/create_listing.rs` | Init `ListingAccount` | | Create | `programs/solisting/src/instructions/create_listing.rs` | Init `ListingAccount` |
| Create | `programs/solisting/src/instructions/update_listing.rs` | Mutate price/quantity/currencies | | Create | `programs/solisting/src/instructions/update_listing.rs` | Mutate price/quantity/currencies |
@@ -87,9 +88,9 @@ custom-panic = []
anchor-lang = "1.0.2" anchor-lang = "1.0.2"
descro = { path = "../descro", features = ["cpi"] } descro = { path = "../descro", features = ["cpi"] }
descro_ext_resolvers = { path = "../descro_ext_resolvers" } descro_ext_resolvers = { path = "../descro_ext_resolvers" }
# Pyth oracle SDK for on-chain price reads. # Pyth oracle SDK for on-chain price reads (Pull Oracle / V2 accounts).
# Verify latest compatible version at https://crates.io/crates/pyth-sdk-solana # Verify latest compatible version at https://crates.io/crates/pyth-solana-receiver-sdk
pyth-sdk-solana = "0.10" pyth-solana-receiver-sdk = "0.3"
[dev-dependencies] [dev-dependencies]
litesvm = "0.10.0" litesvm = "0.10.0"
@@ -140,11 +141,32 @@ git commit -m "chore(solisting): add crate skeleton and workspace entry"
use anchor_lang::prelude::*; use anchor_lang::prelude::*;
/// Payment currency identifier. Amounts are always in the currency's smallest unit: /// Payment currency identifier. Amounts are always in the currency's smallest unit:
/// Sol → Lamports (u64), Spl → token units defined by the mint's decimals field. /// Sol → Lamports (u64), Spl → token units (smallest unit per `decimals`).
#[derive(AnchorSerialize, AnchorDeserialize, Clone, InitSpace, Debug, PartialEq)] #[derive(AnchorSerialize, AnchorDeserialize, Clone, InitSpace, Debug, PartialEq)]
pub enum Currency { pub enum Currency {
Sol, Sol,
Spl { mint: Pubkey }, /// `decimals` mirrors the SPL mint's decimals field (e.g. 6 for USDC/USDT, 5 for BONK).
/// Storing it here avoids a mint account lookup at order time.
Spl { mint: Pubkey, decimals: u8 },
}
impl Currency {
pub fn decimals(&self) -> u8 {
match self {
Currency::Sol => 9,
Currency::Spl { decimals, .. } => *decimals,
}
}
}
/// Per-alt-currency oracle configuration. Each alt currency carries its own Pyth feed
/// because different tokens have different price feeds.
#[derive(AnchorSerialize, AnchorDeserialize, Clone, InitSpace, Debug)]
pub struct AltCurrencyConfig {
pub currency: Currency,
/// Pyth V2 PriceFeedAccount for this token priced in USD (TOKEN/USD).
/// `None` = treat as a $1.00 USD stablecoin — no oracle account required at order time.
pub usd_oracle: Option<Pubkey>,
} }
#[account] #[account]
@@ -154,14 +176,15 @@ pub struct ListingAccount {
/// Sole source of truth for price. All alt_currencies are derived from this via oracle. /// Sole source of truth for price. All alt_currencies are derived from this via oracle.
pub canonical_currency: Currency, pub canonical_currency: Currency,
/// Price in canonical_currency's smallest unit. /// Price in canonical_currency's smallest unit.
pub canonical_amount: u64, pub price: u64,
/// Other currencies the seller accepts. Amounts are always oracle-computed at create_order /// Pyth V2 PriceFeedAccount for the canonical currency priced in USD.
/// time — never stored independently. Empty = oracle never needed. /// `None` = canonical is a $1.00 USD stablecoin.
/// For Sol canonical: set to the SOL/USD Pyth feed.
pub canonical_oracle: Option<Pubkey>,
/// Alt currencies the seller accepts. Amounts are oracle-computed at create_order time.
/// Empty = no oracle ever needed.
#[max_len(3)] #[max_len(3)]
pub alt_currencies: Vec<Currency>, pub alt_currencies: Vec<AltCurrencyConfig>,
/// Pyth Price Feed account. Must be Some when alt_currencies is non-empty.
/// The seller chooses which oracle they trust.
pub price_oracle: Option<Pubkey>,
/// Resolvers the seller accepts for dispute resolution. Empty = any resolver ok. /// Resolvers the seller accepts for dispute resolution. Empty = any resolver ok.
#[max_len(4)] #[max_len(4)]
pub accepted_resolvers: Vec<Pubkey>, pub accepted_resolvers: Vec<Pubkey>,
@@ -185,7 +208,7 @@ pub struct OrderAccount {
pub resolver: Pubkey, pub resolver: 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 canonical_amount when oracle-converted). /// Actual amount paid (may differ from price when oracle-converted).
pub amount: u64, pub amount: u64,
/// The descro EscrowAccount PDA for this order. /// The descro EscrowAccount PDA for this order.
pub escrow_account: Pubkey, pub escrow_account: Pubkey,
@@ -214,9 +237,9 @@ pub enum SolistingError {
CurrencyNotAccepted, CurrencyNotAccepted,
#[msg("Resolver is not accepted by this listing")] #[msg("Resolver is not accepted by this listing")]
ResolverNotAccepted, ResolverNotAccepted,
#[msg("alt_currencies is non-empty but price_oracle is None on the listing")] #[msg("Oracle account is required but was not provided (non-stablecoin currency)")]
OracleRequired, OracleRequired,
#[msg("Oracle account key does not match listing.price_oracle")] #[msg("Passed oracle account key does not match the address stored in the listing")]
OracleMismatch, OracleMismatch,
#[msg("Oracle price is unavailable, stale, or has too wide a confidence interval")] #[msg("Oracle price is unavailable, stale, or has too wide a confidence interval")]
OraclePriceUnavailable, OraclePriceUnavailable,
@@ -233,102 +256,113 @@ pub enum SolistingError {
- [ ] **Step 3: Write `oracle.rs`** - [ ] **Step 3: Write `oracle.rs`**
The oracle module reads a Pyth SOL/USD price feed and converts between SOL (Lamports) and a The oracle module converts between any two currencies using USD as an intermediate. Each
stablecoin (6-decimal SPL token, e.g. USDT/USDC). Stablecoins are treated as exactly $1.00 — currency has an optional Pyth feed (TOKEN/USD). `None` = treat as $1.00 stablecoin. No feed ID
no second feed needed for the stable side. is hardcoded — the seller registers the oracle account address in the listing; the program only
verifies that the passed account key matches the stored key (done in `create_order`, not here).
Math (using USD as pivot):
```
canonical_usd = price × (cp_raw × 10^cp_expo) / 10^cd
target_amount = canonical_usd × 10^td / (tp_raw × 10^tp_expo)
= price × cp_raw / tp_raw × 10^(cp_expo tp_expo + td cd)
let shift = cp_expo tp_expo + td cd (can be positive or negative)
```
Stablecoin sentinel: `(1, 0)` meaning exactly $1.00 per whole token.
Example — SOL → BONK (cd=9, cp_expo=8, td=5, tp_expo=8):
`shift = 8 (8) + 5 9 = 4``target = ca × cp_raw / tp_raw / 10^4`
Example — SOL → USDC (cd=9, cp_expo=8, td=6, tp_raw=1, tp_expo=0 sentinel):
`shift = 8 0 + 6 9 = 11``target = ca × cp_raw / 10^11` ✓ (matches old formula)
```rust ```rust
use anchor_lang::prelude::*; use anchor_lang::prelude::*;
use pyth_sdk_solana::load_price_feed_from_account_info; use anchor_lang::AccountDeserialize;
use pyth_solana_receiver_sdk::price_update::PriceUpdateV2;
use crate::error::SolistingError; use crate::error::SolistingError;
use crate::state::Currency; use crate::state::Currency;
/// Maximum age of the oracle price in seconds before it is considered stale. const ORACLE_MAX_AGE_SECS: i64 = 60;
const ORACLE_MAX_AGE_SECS: u64 = 60;
/// Reads the Pyth SOL/USD price feed and returns (price_i64, exponent_i32). /// Reads any Pyth V2 PriceFeedAccount and returns (price_i64, exponent_i32) where
/// price_i64 * 10^exponent = USD per SOL. /// `price * 10^exponent` is the USD value of 1 whole token (price in USD per whole unit).
/// Returns OraclePriceUnavailable if the price is stale or the feed account is invalid. /// The caller is responsible for verifying the account key matches listing state.
fn sol_usd_price(oracle_account: &AccountInfo) -> Result<(i64, i32)> { fn read_usd_price(oracle_account: &AccountInfo) -> Result<(i64, i32)> {
let feed = load_price_feed_from_account_info(oracle_account) let data = oracle_account.try_borrow_data()?;
let price_update = PriceUpdateV2::try_deserialize(&mut data.as_ref())
.map_err(|_| error!(SolistingError::OraclePriceUnavailable))?; .map_err(|_| error!(SolistingError::OraclePriceUnavailable))?;
let msg = &price_update.price_message;
let now = Clock::get()?.unix_timestamp; let now = Clock::get()?.unix_timestamp;
let price = feed require!(
.get_price_no_older_than(now, ORACLE_MAX_AGE_SECS) now - msg.publish_time <= ORACLE_MAX_AGE_SECS,
.ok_or(error!(SolistingError::OraclePriceUnavailable))?; SolistingError::OraclePriceUnavailable
Ok((price.price, price.expo)) );
require!(msg.price > 0, SolistingError::OraclePriceUnavailable);
Ok((msg.price, msg.exponent))
} }
/// Converts `canonical_amount` (in `canonical_currency`) to `target_currency` units. /// Returns the USD price as (raw_i64, exponent_i32) for a currency.
/// Stablecoin sentinel (oracle = None) returns (1, 0) = exactly $1.00 per whole token.
fn usd_price(oracle: Option<&AccountInfo>) -> Result<(i64, i32)> {
match oracle {
None => Ok((1, 0)),
Some(account) => read_usd_price(account),
}
}
/// Converts `price` (smallest units of `canonical_currency`) to the equivalent
/// amount in `target_currency` smallest units, using USD as an intermediate.
/// ///
/// Supported conversions: /// `canonical_oracle` / `target_oracle`: pass `None` when the currency is a USD stablecoin.
/// Sol → Spl (stablecoin 6 decimals): lamports → token_units /// `canonical_currency` / `target_currency`: used only for their `decimals()` value.
/// Spl (stablecoin 6 decimals) → Sol: token_units → lamports
/// ///
/// Formula Sol → Spl (assuming 1 stablecoin = $1, 6 decimals): /// Slippage: if |computed expected_amount| / expected_amount > max_slippage_bps / 10_000,
/// stablecoin_units = lamports * price * 10^expo / 10^9 * 10^6 /// returns `SlippageExceeded`. Pass `max_slippage_bps = 0` to skip the check.
/// = lamports * price * 10^(expo + 6 - 9)
/// = lamports * price * 10^(expo - 3)
/// With expo=-8: stablecoin_units = lamports * price / 10^11
///
/// Uses u128 intermediate to avoid overflow (max ~1.8e19 in u64 is too small).
///
/// Slippage check: if the oracle-computed amount differs from `expected_amount` by more than
/// `max_slippage_bps` basis points, returns SlippageExceeded.
pub fn convert_with_slippage( pub fn convert_with_slippage(
oracle_account: &AccountInfo, canonical_oracle: Option<&AccountInfo>,
canonical_currency: &Currency, canonical_currency: &Currency,
canonical_amount: u64, price: u64,
target_oracle: Option<&AccountInfo>,
target_currency: &Currency, target_currency: &Currency,
expected_amount: u64, expected_amount: u64,
max_slippage_bps: u16, max_slippage_bps: u16,
) -> Result<u64> { ) -> Result<u64> {
let (price_raw, expo) = sol_usd_price(oracle_account)?; let (cp_raw, cp_expo) = usd_price(canonical_oracle)?;
require!(price_raw > 0, SolistingError::OraclePriceUnavailable); let (tp_raw, tp_expo) = usd_price(target_oracle)?;
let computed = match (canonical_currency, target_currency) { let cd = canonical_currency.decimals() as i32;
(Currency::Sol, Currency::Spl { .. }) => { let td = target_currency.decimals() as i32;
// lamports → stablecoin units (6 decimals, $1 peg)
// = lamports * price_raw / 10^(9 - 6 + (-expo)) // shift = cp_expo tp_expo + td cd
// = lamports * price_raw / 10^(3 - expo) [expo is negative, so 3 - expo > 3] let shift: i32 = cp_expo - tp_expo + td - cd;
let shift: u32 = (3i32 - expo) as u32; // expo=-8 → shift=11
let intermediate = (canonical_amount as u128) // target = price × cp_raw / tp_raw × 10^shift
.checked_mul(price_raw as u128) // Use u128 to avoid overflow. tp_raw and cp_raw are always positive (checked above).
.ok_or(error!(SolistingError::OraclePriceUnavailable))?; let numerator = (price as u128)
let divisor = 10u128.pow(shift); .checked_mul(cp_raw as u128)
u64::try_from(intermediate / divisor) .ok_or(error!(SolistingError::OraclePriceUnavailable))?;
.map_err(|_| error!(SolistingError::OraclePriceUnavailable))?
} let computed: u64 = if shift >= 0 {
(Currency::Spl { .. }, Currency::Sol) => { let scaled = numerator
// stablecoin units (6 decimals, $1 peg) → lamports .checked_mul(10u128.pow(shift as u32))
// = token_units * 10^9 / (price_raw * 10^expo) .ok_or(error!(SolistingError::OraclePriceUnavailable))?;
// = token_units * 10^9 / price_raw * 10^(-expo) u64::try_from(scaled / tp_raw as u128)
// = token_units * 10^(9 + (-expo)) / price_raw .map_err(|_| error!(SolistingError::OraclePriceUnavailable))?
// = token_units * 10^(9 - expo) / price_raw [expo negative, so 9 - expo > 9] } else {
// Example expo=-8: = token_units * 10^17 / price_raw let divisor = 10u128.pow((-shift) as u32);
// Then divide out the stablecoin decimals (6): u64::try_from(numerator / divisor / tp_raw as u128)
// = token_units * 10^(9 - expo - 6) / price_raw = token_units * 10^(3 - expo) / price_raw .map_err(|_| error!(SolistingError::OraclePriceUnavailable))?
let shift: u32 = (3i32 - expo) as u32; // expo=-8 → shift=11
let numerator = (canonical_amount as u128)
.checked_mul(10u128.pow(shift))
.ok_or(error!(SolistingError::OraclePriceUnavailable))?;
u64::try_from(numerator / price_raw as u128)
.map_err(|_| error!(SolistingError::OraclePriceUnavailable))?
}
_ => return Err(error!(SolistingError::OraclePriceUnavailable)),
}; };
// Slippage: |computed - expected| / expected <= max_slippage_bps / 10_000 if max_slippage_bps > 0 && expected_amount > 0 {
if max_slippage_bps > 0 {
let tolerance = (expected_amount as u128) let tolerance = (expected_amount as u128)
.checked_mul(max_slippage_bps as u128) .checked_mul(max_slippage_bps as u128)
.unwrap_or(u128::MAX) .unwrap_or(u128::MAX)
/ 10_000; / 10_000;
let diff = if computed > expected_amount { let diff = computed.abs_diff(expected_amount) as u128;
computed as u128 - expected_amount as u128
} else {
expected_amount as u128 - computed as u128
};
require!(diff <= tolerance, SolistingError::SlippageExceeded); require!(diff <= tolerance, SolistingError::SlippageExceeded);
} }
@@ -374,8 +408,8 @@ fn seller_can_create_sol_only_listing() {
listing_id, listing_id,
Currency::Sol, Currency::Sol,
price, price,
None, // canonical_oracle: None (no alt currencies)
vec![], // no alt_currencies vec![], // no alt_currencies
None, // no oracle
vec![], // any resolver vec![], // any resolver
10, // quantity 10, // quantity
"ipfs://test".to_string(), "ipfs://test".to_string(),
@@ -384,39 +418,74 @@ fn seller_can_create_sol_only_listing() {
let listing = read_listing(&svm, &seller.pubkey(), listing_id); let listing = read_listing(&svm, &seller.pubkey(), listing_id);
assert_eq!(listing.canonical_currency, Currency::Sol); assert_eq!(listing.canonical_currency, Currency::Sol);
assert_eq!(listing.canonical_amount, price); assert_eq!(listing.price, price);
assert_eq!(listing.alt_currencies.len(), 0); assert_eq!(listing.alt_currencies.len(), 0);
assert_eq!(listing.price_oracle, None); assert_eq!(listing.canonical_oracle, None);
assert_eq!(listing.quantity, 10); assert_eq!(listing.quantity, 10);
assert_eq!(listing.quantity_reserved, 0); assert_eq!(listing.quantity_reserved, 0);
assert!(listing.is_active); assert!(listing.is_active);
} }
#[test] #[test]
fn create_listing_with_alt_currency_requires_oracle() { fn seller_can_create_listing_with_usdc_alt_stablecoin() {
// alt_currencies non-empty but price_oracle = None → should fail // canonical=Sol, alt=USDC with no oracle (stablecoin $1 peg)
let (mut svm, seller, _) = setup(); let (mut svm, seller, _) = setup();
let usdt_mint = Pubkey::new_unique(); let usdc_mint = Pubkey::new_unique();
let sol_usd_feed = Pubkey::new_unique(); // mock SOL/USD feed key
let ix = ix_create_listing( let ix = ix_create_listing(
&seller.pubkey(), &seller.pubkey(),
42, 10,
Currency::Sol, Currency::Sol,
1_000_000_000, 1_000_000_000,
vec![Currency::Spl { mint: usdt_mint }], Some(sol_usd_feed), // canonical_oracle = SOL/USD feed
None, // oracle missing — must fail vec![AltCurrencyConfig {
currency: Currency::Spl { mint: usdc_mint, decimals: 6 },
usd_oracle: None, // USDC is a stablecoin — no feed needed
}],
vec![], vec![],
5, 5,
"ipfs://x".to_string(), "ipfs://x".to_string(),
); );
let result = try_send(&mut svm, &[ix], &[&seller]); send(&mut svm, &[ix], &[&seller]);
assert!(result.is_err());
let listing = read_listing(&svm, &seller.pubkey(), 10);
assert_eq!(listing.canonical_oracle, Some(sol_usd_feed));
assert_eq!(listing.alt_currencies[0].usd_oracle, None);
}
#[test]
fn seller_can_create_listing_with_bonk_alt_oracle() {
// canonical=Sol, alt=BONK — needs both SOL/USD and BONK/USD feeds
let (mut svm, seller, _) = setup();
let bonk_mint = Pubkey::new_unique();
let sol_usd_feed = Pubkey::new_unique();
let bonk_usd_feed = Pubkey::new_unique();
let ix = ix_create_listing(
&seller.pubkey(),
11,
Currency::Sol,
1_000_000_000,
Some(sol_usd_feed),
vec![AltCurrencyConfig {
currency: Currency::Spl { mint: bonk_mint, decimals: 5 },
usd_oracle: Some(bonk_usd_feed),
}],
vec![],
5,
"".to_string(),
);
send(&mut svm, &[ix], &[&seller]);
let listing = read_listing(&svm, &seller.pubkey(), 11);
assert_eq!(listing.canonical_oracle, Some(sol_usd_feed));
assert_eq!(listing.alt_currencies[0].usd_oracle, Some(bonk_usd_feed));
} }
#[test] #[test]
fn seller_can_update_listing() { fn seller_can_update_listing() {
let (mut svm, seller, _) = setup(); let (mut svm, seller, _) = setup();
let listing_id: u64 = 2; let listing_id: u64 = 2;
let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000, vec![], None, vec![], 10, "".to_string()); let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000, None, vec![], vec![], 10, "".to_string());
send(&mut svm, &[ix], &[&seller]); send(&mut svm, &[ix], &[&seller]);
let ix_update = ix_update_listing( let ix_update = ix_update_listing(
@@ -424,16 +493,16 @@ fn seller_can_update_listing() {
listing_id, listing_id,
Currency::Sol, Currency::Sol,
2_000_000_000, 2_000_000_000,
vec![],
None, None,
vec![], vec![],
vec![],
20, 20,
"ipfs://new".to_string(), "ipfs://new".to_string(),
); );
send(&mut svm, &[ix_update], &[&seller]); send(&mut svm, &[ix_update], &[&seller]);
let listing = read_listing(&svm, &seller.pubkey(), listing_id); let listing = read_listing(&svm, &seller.pubkey(), listing_id);
assert_eq!(listing.canonical_amount, 2_000_000_000); assert_eq!(listing.price, 2_000_000_000);
assert_eq!(listing.quantity, 20); assert_eq!(listing.quantity, 20);
} }
@@ -441,7 +510,7 @@ fn seller_can_update_listing() {
fn seller_can_close_listing() { fn seller_can_close_listing() {
let (mut svm, seller, _) = setup(); let (mut svm, seller, _) = setup();
let listing_id: u64 = 3; let listing_id: u64 = 3;
let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000, vec![], None, vec![], 5, "".to_string()); let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000, None, vec![], vec![], 5, "".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);
@@ -489,24 +558,19 @@ pub fn handler(
ctx: Context<CreateListing>, ctx: Context<CreateListing>,
listing_id: u64, listing_id: u64,
canonical_currency: Currency, canonical_currency: Currency,
canonical_amount: u64, price: u64,
alt_currencies: Vec<Currency>, canonical_oracle: Option<Pubkey>,
price_oracle: Option<Pubkey>, alt_currencies: Vec<AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>, accepted_resolvers: Vec<Pubkey>,
quantity: u32, quantity: u32,
metadata_uri: String, metadata_uri: String,
) -> Result<()> { ) -> Result<()> {
require!(
alt_currencies.is_empty() || price_oracle.is_some(),
SolistingError::OracleRequired
);
let listing = &mut ctx.accounts.listing_account; let listing = &mut ctx.accounts.listing_account;
listing.seller = ctx.accounts.seller.key(); listing.seller = ctx.accounts.seller.key();
listing.canonical_currency = canonical_currency; listing.canonical_currency = canonical_currency;
listing.canonical_amount = canonical_amount; listing.price = price;
listing.canonical_oracle = canonical_oracle;
listing.alt_currencies = alt_currencies; listing.alt_currencies = alt_currencies;
listing.price_oracle = price_oracle;
listing.accepted_resolvers = accepted_resolvers; listing.accepted_resolvers = accepted_resolvers;
listing.quantity = quantity; listing.quantity = quantity;
listing.quantity_reserved = 0; listing.quantity_reserved = 0;
@@ -542,23 +606,18 @@ pub struct UpdateListing<'info> {
pub fn handler( pub fn handler(
ctx: Context<UpdateListing>, ctx: Context<UpdateListing>,
canonical_currency: Currency, canonical_currency: Currency,
canonical_amount: u64, price: u64,
alt_currencies: Vec<Currency>, canonical_oracle: Option<Pubkey>,
price_oracle: Option<Pubkey>, alt_currencies: Vec<AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>, accepted_resolvers: Vec<Pubkey>,
quantity: u32, quantity: u32,
metadata_uri: String, metadata_uri: String,
) -> Result<()> { ) -> Result<()> {
require!(
alt_currencies.is_empty() || price_oracle.is_some(),
SolistingError::OracleRequired
);
let listing = &mut ctx.accounts.listing_account; let listing = &mut ctx.accounts.listing_account;
listing.canonical_currency = canonical_currency; listing.canonical_currency = canonical_currency;
listing.canonical_amount = canonical_amount; listing.price = price;
listing.canonical_oracle = canonical_oracle;
listing.alt_currencies = alt_currencies; listing.alt_currencies = alt_currencies;
listing.price_oracle = price_oracle;
listing.accepted_resolvers = accepted_resolvers; listing.accepted_resolvers = accepted_resolvers;
listing.quantity = quantity; listing.quantity = quantity;
listing.metadata_uri = metadata_uri; listing.metadata_uri = metadata_uri;
@@ -643,9 +702,9 @@ pub fn ix_create_listing(
seller: &Pubkey, seller: &Pubkey,
listing_id: u64, listing_id: u64,
canonical_currency: Currency, canonical_currency: Currency,
canonical_amount: u64, price: u64,
alt_currencies: Vec<Currency>, canonical_oracle: Option<Pubkey>,
price_oracle: Option<Pubkey>, alt_currencies: Vec<AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>, accepted_resolvers: Vec<Pubkey>,
quantity: u32, quantity: u32,
metadata_uri: String, metadata_uri: String,
@@ -657,9 +716,9 @@ pub fn ix_update_listing(
seller: &Pubkey, seller: &Pubkey,
listing_id: u64, listing_id: u64,
canonical_currency: Currency, canonical_currency: Currency,
canonical_amount: u64, price: u64,
alt_currencies: Vec<Currency>, canonical_oracle: Option<Pubkey>,
price_oracle: Option<Pubkey>, alt_currencies: Vec<AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>, accepted_resolvers: Vec<Pubkey>,
quantity: u32, quantity: u32,
metadata_uri: String, metadata_uri: String,
@@ -712,32 +771,32 @@ pub mod solisting {
ctx: Context<CreateListing>, ctx: Context<CreateListing>,
listing_id: u64, listing_id: u64,
canonical_currency: state::Currency, canonical_currency: state::Currency,
canonical_amount: u64, price: u64,
alt_currencies: Vec<state::Currency>, canonical_oracle: Option<Pubkey>,
price_oracle: Option<Pubkey>, alt_currencies: Vec<state::AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>, accepted_resolvers: Vec<Pubkey>,
quantity: u32, quantity: u32,
metadata_uri: String, metadata_uri: String,
) -> Result<()> { ) -> Result<()> {
create_listing::handler( create_listing::handler(
ctx, listing_id, canonical_currency, canonical_amount, ctx, listing_id, canonical_currency, price,
alt_currencies, price_oracle, accepted_resolvers, quantity, metadata_uri, canonical_oracle, alt_currencies, accepted_resolvers, quantity, metadata_uri,
) )
} }
pub fn update_listing( pub fn update_listing(
ctx: Context<UpdateListing>, ctx: Context<UpdateListing>,
canonical_currency: state::Currency, canonical_currency: state::Currency,
canonical_amount: u64, price: u64,
alt_currencies: Vec<state::Currency>, canonical_oracle: Option<Pubkey>,
price_oracle: Option<Pubkey>, alt_currencies: Vec<state::AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>, accepted_resolvers: Vec<Pubkey>,
quantity: u32, quantity: u32,
metadata_uri: String, metadata_uri: String,
) -> Result<()> { ) -> Result<()> {
update_listing::handler( update_listing::handler(
ctx, canonical_currency, canonical_amount, ctx, canonical_currency, price,
alt_currencies, price_oracle, accepted_resolvers, quantity, metadata_uri, canonical_oracle, alt_currencies, accepted_resolvers, quantity, metadata_uri,
) )
} }
@@ -785,12 +844,12 @@ fn buyer_can_create_order_canonical_sol() {
// Buyer creates order paying in Sol // Buyer creates order paying in Sol
// Assert: OrderAccount exists with payment_currency=Sol, amount=0.1 SOL in lamports // Assert: OrderAccount exists with payment_currency=Sol, amount=0.1 SOL in lamports
// Assert: descro EscrowAccount exists, state == AwaitingSellerConfirm // Assert: descro EscrowAccount exists, state == AwaitingSellerConfirm
// Assert: descro vault has listing.canonical_amount lamports // Assert: descro vault has listing.price lamports
// Assert: listing.quantity_reserved == 1 // Assert: listing.quantity_reserved == 1
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 1u64; let listing_id = 1u64;
let price = 100_000_000u64; let price = 100_000_000u64;
let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, vec![], None, vec![], 5, "".to_string()); let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, None, vec![], vec![], 5, "".to_string());
send(&mut svm, &[ix], &[&seller]); send(&mut svm, &[ix], &[&seller]);
let order_id = 1u64; let order_id = 1u64;
@@ -817,7 +876,7 @@ fn buyer_can_create_order_canonical_sol() {
fn create_order_fails_if_listing_inactive() { fn create_order_fails_if_listing_inactive() {
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 2u64; let listing_id = 2u64;
let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000, vec![], None, vec![], 1, "".to_string()); let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000, None, vec![], vec![], 1, "".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);
send(&mut svm, &[ix_close], &[&seller]); send(&mut svm, &[ix_close], &[&seller]);
@@ -833,13 +892,13 @@ fn create_order_fails_if_out_of_stock() {
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 3u64; let listing_id = 3u64;
// quantity = 1, reserve it with first order // quantity = 1, reserve it with first order
let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, vec![], None, vec![], 1, "".to_string()); let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 1, "".to_string());
send(&mut svm, &[ix], &[&seller]); send(&mut svm, &[ix], &[&seller]);
send(&mut svm, &[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None)], &[&buyer]); send(&mut svm, &[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None)], &[&buyer]);
let buyer2 = Keypair::new(); let buyer2 = Keypair::new();
svm.airdrop(&buyer2.pubkey(), 10_000_000_000).unwrap(); svm.airdrop(&buyer2.pubkey(), 10_000_000_000).unwrap();
let result = try_send(&mut svm, &[ix_create_order(&buyer2.pubkey(), &seller.pubkey(), listing_id, 2, Currency::Sol, 0, None)], &[&buyer2]); let result = try_send(&mut svm, &[ix_create_order(&buyer2.pubkey(), &seller.pubkey(), listing_id, 2, Currency::Sol, 0, None, None)], &[&buyer2]);
assert!(result.is_err()); assert!(result.is_err());
} }
@@ -848,12 +907,12 @@ fn create_order_fails_if_currency_not_accepted() {
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 4u64; let listing_id = 4u64;
// Sol-only listing — buyer tries to pay with fake SPL // Sol-only listing — buyer tries to pay with fake SPL
let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, vec![], None, vec![], 5, "".to_string()); let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string());
send(&mut svm, &[ix], &[&seller]); send(&mut svm, &[ix], &[&seller]);
let fake_mint = Pubkey::new_unique(); let fake_mint = Pubkey::new_unique();
let result = try_send(&mut svm, &[ix_create_order( let result = try_send(&mut svm, &[ix_create_order(
&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Spl { mint: fake_mint }, 0, None, &buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Spl { mint: fake_mint, decimals: 6 }, 0, None, None,
)], &[&buyer]); )], &[&buyer]);
assert!(result.is_err()); assert!(result.is_err());
} }
@@ -863,12 +922,12 @@ fn create_order_fails_if_resolver_not_accepted() {
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 5u64; let listing_id = 5u64;
let allowed_resolver = Pubkey::new_unique(); let allowed_resolver = Pubkey::new_unique();
let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, vec![], None, vec![allowed_resolver], 5, "".to_string()); let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![allowed_resolver], 5, "".to_string());
send(&mut svm, &[ix], &[&seller]); send(&mut svm, &[ix], &[&seller]);
// Pass a different resolver — must fail // Pass a different resolver — must fail
let result = try_send(&mut svm, &[ix_create_order_with_resolver( let result = try_send(&mut svm, &[ix_create_order_with_resolver(
&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, Pubkey::new_unique(), &buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None, Pubkey::new_unique(),
)], &[&buyer]); )], &[&buyer]);
assert!(result.is_err()); assert!(result.is_err());
} }
@@ -936,9 +995,14 @@ pub struct CreateOrder<'info> {
)] )]
pub descro_vault: UncheckedAccount<'info>, pub descro_vault: UncheckedAccount<'info>,
/// CHECK: Pyth price feed account. Required when payment_currency != canonical_currency. /// CHECK: Pyth USD feed for the canonical currency (listing.canonical_oracle).
/// Pass SystemProgram ID as a no-op placeholder when oracle is not needed. /// Pass SystemProgram as placeholder when canonical_oracle is None (stablecoin).
pub oracle: UncheckedAccount<'info>, pub canonical_oracle: UncheckedAccount<'info>,
/// CHECK: Pyth USD feed for the chosen alt currency (alt_cfg.usd_oracle).
/// Pass SystemProgram as placeholder when usd_oracle is None (stablecoin).
/// Also pass SystemProgram when paying in the canonical currency (oracle unused).
pub target_oracle: UncheckedAccount<'info>,
/// CHECK: Descro program — verified in handler /// CHECK: Descro program — verified in handler
pub descro_program: UncheckedAccount<'info>, pub descro_program: UncheckedAccount<'info>,
@@ -972,24 +1036,37 @@ pub fn handler(
// Determine amount in payment_currency // Determine amount in payment_currency
let amount = if payment_currency == listing.canonical_currency { let amount = if payment_currency == listing.canonical_currency {
// No oracle needed — pay canonical amount directly // Paying in canonical currency — no oracle needed
listing.canonical_amount listing.price
} else { } else {
// Must be an accepted alt_currency // Must be a configured alt_currency
require!( let alt_cfg = listing.alt_currencies
listing.alt_currencies.contains(&payment_currency), .iter()
SolistingError::CurrencyNotAccepted .find(|c| c.currency == payment_currency)
); .ok_or(error!(SolistingError::CurrencyNotAccepted))?;
// Oracle must be configured on listing
let oracle_key = listing.price_oracle.ok_or(error!(SolistingError::OracleRequired))?; // Validate passed oracle accounts against listing state
require!( if let Some(expected_canonical) = listing.canonical_oracle {
ctx.accounts.oracle.key() == oracle_key, require!(
SolistingError::OracleMismatch ctx.accounts.canonical_oracle.key() == expected_canonical,
); SolistingError::OracleMismatch
);
}
if let Some(expected_target) = alt_cfg.usd_oracle {
require!(
ctx.accounts.target_oracle.key() == expected_target,
SolistingError::OracleMismatch
);
}
let c_oracle = listing.canonical_oracle.map(|_| ctx.accounts.canonical_oracle.as_ref());
let t_oracle = alt_cfg.usd_oracle.map(|_| ctx.accounts.target_oracle.as_ref());
oracle::convert_with_slippage( oracle::convert_with_slippage(
ctx.accounts.oracle.as_ref(), c_oracle,
&listing.canonical_currency, &listing.canonical_currency,
listing.canonical_amount, listing.price,
t_oracle,
&payment_currency, &payment_currency,
expected_amount, expected_amount,
max_slippage_bps, max_slippage_bps,
@@ -1057,7 +1134,8 @@ pub fn read_order(svm: &LiteSVM, listing: Pubkey, buyer: Pubkey, order_id: u64)
solisting::state::OrderAccount::try_deserialize(&mut account.data.as_slice()).unwrap() solisting::state::OrderAccount::try_deserialize(&mut account.data.as_slice()).unwrap()
} }
// ix_create_order: derive escrow_id from order_pda bytes[0..8], pass SystemProgram as oracle placeholder /// `canonical_oracle` / `target_oracle`: pass None to use SystemProgram as placeholder
/// (i.e. when paying in canonical currency or when the oracle is a stablecoin None).
pub fn ix_create_order( pub fn ix_create_order(
buyer: &Pubkey, buyer: &Pubkey,
seller: &Pubkey, seller: &Pubkey,
@@ -1065,13 +1143,14 @@ pub fn ix_create_order(
order_id: u64, order_id: u64,
payment_currency: Currency, payment_currency: Currency,
max_slippage_bps: u16, max_slippage_bps: u16,
oracle: Option<Pubkey>, canonical_oracle: Option<Pubkey>,
target_oracle: Option<Pubkey>,
) -> solana_message::compiled_instruction::CompiledInstruction { ) -> solana_message::compiled_instruction::CompiledInstruction {
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);
let escrow_id = u64::from_le_bytes(order.to_bytes()[0..8].try_into().unwrap()); let escrow_id = u64::from_le_bytes(order.to_bytes()[0..8].try_into().unwrap());
let resolver = Pubkey::new_unique(); // default resolver for simple tests let resolver = Pubkey::new_unique(); // default resolver for simple tests
ix_create_order_with_resolver(buyer, seller, listing_id, order_id, payment_currency, max_slippage_bps, oracle, resolver) ix_create_order_with_resolver(buyer, seller, listing_id, order_id, payment_currency, max_slippage_bps, canonical_oracle, target_oracle, resolver)
} }
pub fn ix_create_order_with_resolver( pub fn ix_create_order_with_resolver(
@@ -1081,7 +1160,8 @@ pub fn ix_create_order_with_resolver(
order_id: u64, order_id: u64,
payment_currency: Currency, payment_currency: Currency,
max_slippage_bps: u16, max_slippage_bps: u16,
oracle: Option<Pubkey>, canonical_oracle: Option<Pubkey>,
target_oracle: Option<Pubkey>,
resolver: Pubkey, resolver: Pubkey,
) -> solana_message::compiled_instruction::CompiledInstruction { ) -> solana_message::compiled_instruction::CompiledInstruction {
let listing = listing_pda(seller, listing_id); let listing = listing_pda(seller, listing_id);
@@ -1095,9 +1175,10 @@ pub fn ix_create_order_with_resolver(
&[b"vault", escrow_account.as_ref()], &[b"vault", escrow_account.as_ref()],
&descro::id(), &descro::id(),
); );
let oracle_key = oracle.unwrap_or(solana_sdk::system_program::id()); let canonical_oracle_key = canonical_oracle.unwrap_or(solana_sdk::system_program::id());
let target_oracle_key = target_oracle.unwrap_or(solana_sdk::system_program::id());
let expected_amount = 0u64; // tests that don't use oracle set this to 0 let expected_amount = 0u64; // tests that don't use oracle set this to 0
todo!("build CompiledInstruction: discriminator + borsh encode (order_id, escrow_id, resolver, payment_currency, expected_amount, max_slippage_bps), accounts list") todo!("build CompiledInstruction: discriminator + borsh encode (order_id, escrow_id, resolver, payment_currency, expected_amount, max_slippage_bps), accounts: [..., canonical_oracle_key, target_oracle_key, ...]")
} }
``` ```
@@ -1133,9 +1214,9 @@ fn seller_accept_creates_active_descro_escrow() {
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 10u64; let listing_id = 10u64;
let price = 100_000_000u64; let price = 100_000_000u64;
send(&mut svm, &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, vec![], None, vec![], 5, "".to_string())], &[&seller]); send(&mut svm, &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, None, vec![], vec![], 5, "".to_string())], &[&seller]);
let order_id = 1u64; let order_id = 1u64;
send(&mut svm, &[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, order_id, Currency::Sol, 0, None)], &[&buyer]); send(&mut svm, &[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, order_id, Currency::Sol, 0, None, None)], &[&buyer]);
let listing_key = listing_pda(&seller.pubkey(), listing_id); let listing_key = listing_pda(&seller.pubkey(), listing_id);
let order = read_order(&svm, listing_key, buyer.pubkey(), order_id); let order = read_order(&svm, listing_key, buyer.pubkey(), order_id);
@@ -1262,8 +1343,8 @@ fn seller_can_reject_order() {
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 20u64; let listing_id = 20u64;
let price = 100_000_000u64; let price = 100_000_000u64;
send(&mut svm, &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, vec![], None, vec![], 5, "".to_string())], &[&seller]); send(&mut svm, &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, None, vec![], vec![], 5, "".to_string())], &[&seller]);
send(&mut svm, &[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None)], &[&buyer]); send(&mut svm, &[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None)], &[&buyer]);
let buyer_before = svm.get_account(&buyer.pubkey()).map(|a| a.lamports).unwrap_or(0); let buyer_before = svm.get_account(&buyer.pubkey()).map(|a| a.lamports).unwrap_or(0);
let listing_key = listing_pda(&seller.pubkey(), listing_id); let listing_key = listing_pda(&seller.pubkey(), listing_id);
@@ -1281,8 +1362,8 @@ fn seller_can_reject_order() {
fn buyer_can_cancel_order() { fn buyer_can_cancel_order() {
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 21u64; let listing_id = 21u64;
send(&mut svm, &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, vec![], None, vec![], 5, "".to_string())], &[&seller]); send(&mut svm, &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string())], &[&seller]);
send(&mut svm, &[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None)], &[&buyer]); send(&mut svm, &[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None)], &[&buyer]);
let listing_key = listing_pda(&seller.pubkey(), listing_id); let listing_key = listing_pda(&seller.pubkey(), listing_id);
let order = read_order(&svm, listing_key, buyer.pubkey(), 1); let order = read_order(&svm, listing_key, buyer.pubkey(), 1);
@@ -1296,8 +1377,8 @@ fn reject_handles_already_cancelled_escrow() {
// Buyer cancels directly on descro, then seller calls reject_order — must not panic // Buyer cancels directly on descro, then seller calls reject_order — must not panic
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 22u64; let listing_id = 22u64;
send(&mut svm, &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, vec![], None, vec![], 5, "".to_string())], &[&seller]); send(&mut svm, &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string())], &[&seller]);
send(&mut svm, &[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None)], &[&buyer]); send(&mut svm, &[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None)], &[&buyer]);
let listing_key = listing_pda(&seller.pubkey(), listing_id); let listing_key = listing_pda(&seller.pubkey(), listing_id);
let order = read_order(&svm, listing_key, buyer.pubkey(), 1); let order = read_order(&svm, listing_key, buyer.pubkey(), 1);
@@ -1518,8 +1599,8 @@ Anyone can call this to close an `OrderAccount` whose descro escrow is in a term
fn anyone_can_close_stale_order_after_terminal_escrow() { fn anyone_can_close_stale_order_after_terminal_escrow() {
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 30u64; let listing_id = 30u64;
send(&mut svm, &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, vec![], None, vec![], 5, "".to_string())], &[&seller]); send(&mut svm, &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string())], &[&seller]);
send(&mut svm, &[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None)], &[&buyer]); send(&mut svm, &[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None)], &[&buyer]);
let listing_key = listing_pda(&seller.pubkey(), listing_id); let listing_key = listing_pda(&seller.pubkey(), listing_id);
let order = read_order(&svm, listing_key, buyer.pubkey(), 1); let order = read_order(&svm, listing_key, buyer.pubkey(), 1);
@@ -1541,8 +1622,8 @@ fn anyone_can_close_stale_order_after_terminal_escrow() {
fn close_stale_order_fails_if_escrow_still_active() { fn close_stale_order_fails_if_escrow_still_active() {
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 31u64; let listing_id = 31u64;
send(&mut svm, &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, vec![], None, vec![], 5, "".to_string())], &[&seller]); send(&mut svm, &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string())], &[&seller]);
send(&mut svm, &[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None)], &[&buyer]); send(&mut svm, &[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None)], &[&buyer]);
let listing_key = listing_pda(&seller.pubkey(), listing_id); let listing_key = listing_pda(&seller.pubkey(), listing_id);
let order = read_order(&svm, listing_key, buyer.pubkey(), 1); let order = read_order(&svm, listing_key, buyer.pubkey(), 1);
@@ -1664,12 +1745,12 @@ declare_id!("So1istingProgramID11111111111111111111111111"); // replace with: so
pub mod solisting { pub mod solisting {
use super::*; use super::*;
pub fn create_listing(ctx: Context<CreateListing>, listing_id: u64, canonical_currency: state::Currency, canonical_amount: u64, alt_currencies: Vec<state::Currency>, price_oracle: Option<Pubkey>, accepted_resolvers: Vec<Pubkey>, quantity: u32, metadata_uri: String) -> Result<()> { pub fn create_listing(ctx: Context<CreateListing>, listing_id: u64, canonical_currency: state::Currency, price: u64, canonical_oracle: Option<Pubkey>, alt_currencies: Vec<state::AltCurrencyConfig>, accepted_resolvers: Vec<Pubkey>, quantity: u32, metadata_uri: String) -> Result<()> {
create_listing::handler(ctx, listing_id, canonical_currency, canonical_amount, alt_currencies, price_oracle, accepted_resolvers, quantity, metadata_uri) create_listing::handler(ctx, listing_id, canonical_currency, price, canonical_oracle, alt_currencies, accepted_resolvers, quantity, metadata_uri)
} }
pub fn update_listing(ctx: Context<UpdateListing>, canonical_currency: state::Currency, canonical_amount: u64, alt_currencies: Vec<state::Currency>, price_oracle: Option<Pubkey>, accepted_resolvers: Vec<Pubkey>, quantity: u32, metadata_uri: String) -> Result<()> { pub fn update_listing(ctx: Context<UpdateListing>, canonical_currency: state::Currency, price: u64, canonical_oracle: Option<Pubkey>, alt_currencies: Vec<state::AltCurrencyConfig>, accepted_resolvers: Vec<Pubkey>, quantity: u32, metadata_uri: String) -> Result<()> {
update_listing::handler(ctx, canonical_currency, canonical_amount, alt_currencies, price_oracle, accepted_resolvers, quantity, metadata_uri) update_listing::handler(ctx, canonical_currency, price, canonical_oracle, alt_currencies, accepted_resolvers, quantity, metadata_uri)
} }
pub fn close_listing(ctx: Context<CloseListing>) -> Result<()> { pub fn close_listing(ctx: Context<CloseListing>) -> Result<()> {
@@ -1677,6 +1758,7 @@ pub mod solisting {
} }
pub fn create_order(ctx: Context<CreateOrder>, order_id: u64, escrow_id: u64, resolver: Pubkey, payment_currency: state::Currency, expected_amount: u64, max_slippage_bps: u16) -> Result<()> { pub fn create_order(ctx: Context<CreateOrder>, order_id: u64, escrow_id: u64, resolver: Pubkey, payment_currency: state::Currency, expected_amount: u64, max_slippage_bps: u16) -> Result<()> {
// canonical_oracle and target_oracle come from the account struct, not args
create_order::handler(ctx, order_id, escrow_id, resolver, payment_currency, expected_amount, max_slippage_bps) create_order::handler(ctx, order_id, escrow_id, resolver, payment_currency, expected_amount, max_slippage_bps)
} }

View File

@@ -9,9 +9,6 @@
"lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
}, },
"dependencies": {
"@anchor-lang/core": "^1.0.2"
},
"devDependencies": { "devDependencies": {
"@types/bn.js": "^5.2.0", "@types/bn.js": "^5.2.0",
"@types/chai": "^5.2.3", "@types/chai": "^5.2.3",

View File

@@ -25,7 +25,7 @@ anchor-lang = "1.0.2"
descro_ext_resolvers = { path = "../descro_ext_resolvers", features = ["cpi"] } descro_ext_resolvers = { path = "../descro_ext_resolvers", features = ["cpi"] }
[dev-dependencies] [dev-dependencies]
litesvm = "0.10.0" litesvm = "0.12.0"
solana-message = "3.0.1" solana-message = "3.0.1"
solana-transaction = "3.0.2" solana-transaction = "3.0.2"
solana-signer = "3.0.0" solana-signer = "3.0.0"

View File

@@ -18,4 +18,6 @@ pub enum EscrowError {
InsufficientVaultBalance, InsufficientVaultBalance,
#[msg("Amount must be at least rent-exempt minimum for the vault (~890880 lamports)")] #[msg("Amount must be at least rent-exempt minimum for the vault (~890880 lamports)")]
AmountBelowRentMinimum, AmountBelowRentMinimum,
#[msg("Resolver must co-sign this transaction (SignatureGated acceptance policy)")]
ResolverSignatureRequired,
} }

View File

@@ -1,19 +1,21 @@
#![allow(ambiguous_glob_reexports)] #![allow(ambiguous_glob_reexports)]
pub mod buyer_create_escrow;
pub mod cancel; pub mod cancel;
pub mod complete; pub mod complete;
pub mod create_escrow; pub mod create_escrow;
pub mod create_escrow_prefunded;
pub mod deposit; pub mod deposit;
pub mod dispute; pub mod dispute;
pub mod emergency_resolve; pub mod emergency_resolve;
pub mod resolve; pub mod resolve;
pub mod seller_confirm;
pub use buyer_create_escrow::*;
pub use cancel::*; pub use cancel::*;
pub use complete::*; pub use complete::*;
pub use create_escrow::*; pub use create_escrow::*;
pub use create_escrow_prefunded::*;
pub use deposit::*; pub use deposit::*;
pub use dispute::*; pub use dispute::*;
pub use emergency_resolve::*; pub use emergency_resolve::*;
pub use resolve::*; pub use resolve::*;
pub use seller_confirm::*;

View File

@@ -1,26 +1,27 @@
use anchor_lang::prelude::*;
use crate::state::{EscrowAccount, EscrowState}; use crate::state::{EscrowAccount, EscrowState};
use crate::error::EscrowError; use crate::EscrowError;
use anchor_lang::prelude::*;
use anchor_lang::system_program::{self, Transfer};
#[derive(Accounts)] #[derive(Accounts)]
#[instruction(amount: u64, dispute_resolver: Option<Pubkey>, escrow_id: u64)] #[instruction(amount: u64, dispute_resolver: Option<Pubkey>, escrow_id: u64)]
pub struct CreateEscrowPrefunded<'info> { pub struct BuyerCreateEscrow<'info> {
#[account(mut)] #[account(mut)]
pub seller: Signer<'info>, pub buyer: Signer<'info>,
/// CHECK: Only stored as pubkey, no ownership check needed /// CHECK: Only stored as pubkey, no ownership check needed
pub buyer: UncheckedAccount<'info>, pub seller: UncheckedAccount<'info>,
#[account( #[account(
init, init,
payer = seller, payer = buyer,
space = 8 + EscrowAccount::INIT_SPACE, space = 8 + EscrowAccount::INIT_SPACE,
seeds = [b"escrow", seller.key().as_ref(), &escrow_id.to_le_bytes()], seeds = [b"escrow", seller.key().as_ref(), &escrow_id.to_le_bytes()],
bump bump
)] )]
pub escrow_account: Account<'info, EscrowAccount>, pub escrow_account: Account<'info, EscrowAccount>,
/// CHECK: Pre-funded vault PDA — balance verified in handler /// CHECK: PDA vault for holding escrow SOL; created implicitly on deposit
#[account( #[account(
mut, mut,
seeds = [b"vault", escrow_account.key().as_ref()], seeds = [b"vault", escrow_account.key().as_ref()],
@@ -32,20 +33,31 @@ pub struct CreateEscrowPrefunded<'info> {
} }
pub fn handler( pub fn handler(
ctx: Context<CreateEscrowPrefunded>, ctx: Context<BuyerCreateEscrow>,
amount: u64, amount: u64,
dispute_resolver: Option<Pubkey>, dispute_resolver: Option<Pubkey>,
escrow_id: u64, escrow_id: u64,
) -> Result<()> { ) -> Result<()> {
let vault_lamports = ctx.accounts.vault.to_account_info().lamports(); let rent_min = Rent::get()?.minimum_balance(0);
require!(vault_lamports >= amount, EscrowError::InsufficientVaultBalance); require!(amount >= rent_min, EscrowError::AmountBelowRentMinimum);
system_program::transfer(
CpiContext::new(
system_program::ID,
Transfer {
from: ctx.accounts.buyer.to_account_info(),
to: ctx.accounts.vault.to_account_info(),
},
),
amount,
)?;
let escrow = &mut ctx.accounts.escrow_account; let escrow = &mut ctx.accounts.escrow_account;
escrow.seller = ctx.accounts.seller.key(); escrow.seller = ctx.accounts.seller.key();
escrow.buyer = ctx.accounts.buyer.key(); escrow.buyer = ctx.accounts.buyer.key();
escrow.amount = amount; escrow.amount = amount;
escrow.dispute_resolver = dispute_resolver; escrow.dispute_resolver = dispute_resolver;
escrow.state = EscrowState::Active; escrow.state = EscrowState::AwaitingSellerConfirm;
escrow.bump = ctx.bumps.escrow_account; escrow.bump = ctx.bumps.escrow_account;
escrow.vault_bump = ctx.bumps.vault; escrow.vault_bump = ctx.bumps.vault;
escrow.escrow_id = escrow_id; escrow.escrow_id = escrow_id;

View File

@@ -1,25 +1,66 @@
use anchor_lang::prelude::*; use anchor_lang::prelude::*;
use anchor_lang::system_program::{self, Transfer};
use crate::state::{EscrowAccount, EscrowState}; use crate::state::{EscrowAccount, EscrowState};
use crate::error::EscrowError; use crate::error::EscrowError;
#[derive(Accounts)] #[derive(Accounts)]
pub struct Cancel<'info> { pub struct Cancel<'info> {
#[account(mut)] #[account(mut)]
pub seller: Signer<'info>, pub canceller: Signer<'info>,
/// CHECK: Buyer receives vault refund if funds are present
#[account(
mut,
constraint = buyer.key() == escrow_account.buyer @ EscrowError::Unauthorized,
)]
pub buyer: UncheckedAccount<'info>,
#[account( #[account(
mut, mut,
seeds = [b"escrow", escrow_account.seller.as_ref(), &escrow_account.escrow_id.to_le_bytes()], seeds = [b"escrow", escrow_account.seller.as_ref(), &escrow_account.escrow_id.to_le_bytes()],
bump = escrow_account.bump, bump = escrow_account.bump,
constraint = seller.key() == escrow_account.seller @ EscrowError::Unauthorized, constraint = (
constraint = escrow_account.state == EscrowState::AwaitingDeposit @ EscrowError::InvalidState, canceller.key() == escrow_account.buyer ||
close = seller, canceller.key() == escrow_account.seller
) @ EscrowError::Unauthorized,
constraint = (
escrow_account.state == EscrowState::AwaitingDeposit ||
escrow_account.state == EscrowState::AwaitingSellerConfirm
) @ EscrowError::InvalidState,
close = canceller,
)] )]
pub escrow_account: Account<'info, EscrowAccount>, pub escrow_account: Account<'info, EscrowAccount>,
/// CHECK: Vault PDA — may hold funds (AwaitingSellerConfirm) or be empty (AwaitingDeposit)
#[account(
mut,
seeds = [b"vault", escrow_account.key().as_ref()],
bump = escrow_account.vault_bump,
)]
pub vault: UncheckedAccount<'info>,
pub system_program: Program<'info, System>, pub system_program: Program<'info, System>,
} }
pub fn handler(_ctx: Context<Cancel>) -> Result<()> { pub fn handler(ctx: Context<Cancel>) -> Result<()> {
let vault_lamports = ctx.accounts.vault.to_account_info().lamports();
if vault_lamports > 0 {
let escrow_key = ctx.accounts.escrow_account.key();
let vault_bump = ctx.accounts.escrow_account.vault_bump;
system_program::transfer(
CpiContext::new_with_signer(
system_program::ID,
Transfer {
from: ctx.accounts.vault.to_account_info(),
to: ctx.accounts.buyer.to_account_info(),
},
&[&[b"vault", escrow_key.as_ref(), &[vault_bump]]],
),
vault_lamports,
)?;
}
Ok(()) Ok(())
} }

View File

@@ -11,6 +11,9 @@ pub struct CreateEscrow<'info> {
/// CHECK: Only stored as pubkey, no ownership check needed /// CHECK: Only stored as pubkey, no ownership check needed
pub buyer: UncheckedAccount<'info>, pub buyer: UncheckedAccount<'info>,
/// CHECK: Resolver — may need to co-sign if AcceptancePolicy is SignatureGated
pub resolver: UncheckedAccount<'info>,
#[account( #[account(
init, init,
payer = seller, payer = seller,
@@ -28,6 +31,9 @@ pub struct CreateEscrow<'info> {
)] )]
pub vault: UncheckedAccount<'info>, pub vault: UncheckedAccount<'info>,
/// CHECK: Optional resolver registry entry — read to determine AcceptancePolicy
pub resolver_entry: UncheckedAccount<'info>,
pub system_program: Program<'info, System>, pub system_program: Program<'info, System>,
} }
@@ -40,6 +46,23 @@ pub fn handler(
let rent_min = Rent::get()?.minimum_balance(0); let rent_min = Rent::get()?.minimum_balance(0);
require!(amount >= rent_min, EscrowError::AmountBelowRentMinimum); require!(amount >= rent_min, EscrowError::AmountBelowRentMinimum);
if dispute_resolver.is_some() && !ctx.accounts.resolver_entry.data_is_empty() {
let entry_data = ctx.accounts.resolver_entry.try_borrow_data()?;
let entry = descro_ext_resolvers::state::ResolverEntry::try_deserialize(
&mut entry_data.as_ref(),
)?;
match entry.acceptance_policy {
descro_ext_resolvers::state::AcceptancePolicy::Open => {}
descro_ext_resolvers::state::AcceptancePolicy::SignatureGated => {
require!(
ctx.accounts.resolver.is_signer,
EscrowError::ResolverSignatureRequired
);
}
descro_ext_resolvers::state::AcceptancePolicy::ProgramGated => {}
}
}
let escrow = &mut ctx.accounts.escrow_account; let escrow = &mut ctx.accounts.escrow_account;
escrow.seller = ctx.accounts.seller.key(); escrow.seller = ctx.accounts.seller.key();
escrow.buyer = ctx.accounts.buyer.key(); escrow.buyer = ctx.accounts.buyer.key();

View File

@@ -0,0 +1,47 @@
use anchor_lang::prelude::*;
use crate::state::{EscrowAccount, EscrowState};
use crate::error::EscrowError;
#[derive(Accounts)]
pub struct SellerConfirm<'info> {
pub seller: Signer<'info>,
/// CHECK: Resolver — may need to co-sign if AcceptancePolicy is SignatureGated
pub resolver: UncheckedAccount<'info>,
#[account(
mut,
seeds = [b"escrow", seller.key().as_ref(), &escrow_account.escrow_id.to_le_bytes()],
bump = escrow_account.bump,
constraint = seller.key() == escrow_account.seller @ EscrowError::Unauthorized,
constraint = escrow_account.state == EscrowState::AwaitingSellerConfirm @ EscrowError::InvalidState,
)]
pub escrow_account: Account<'info, EscrowAccount>,
/// CHECK: Optional resolver registry entry — read to determine AcceptancePolicy
pub resolver_entry: UncheckedAccount<'info>,
}
pub fn handler(ctx: Context<SellerConfirm>) -> Result<()> {
if ctx.accounts.escrow_account.dispute_resolver.is_some()
&& !ctx.accounts.resolver_entry.data_is_empty()
{
let entry_data = ctx.accounts.resolver_entry.try_borrow_data()?;
let entry = descro_ext_resolvers::state::ResolverEntry::try_deserialize(
&mut entry_data.as_ref(),
)?;
match entry.acceptance_policy {
descro_ext_resolvers::state::AcceptancePolicy::Open => {}
descro_ext_resolvers::state::AcceptancePolicy::SignatureGated => {
require!(
ctx.accounts.resolver.is_signer,
EscrowError::ResolverSignatureRequired
);
}
descro_ext_resolvers::state::AcceptancePolicy::ProgramGated => {}
}
}
ctx.accounts.escrow_account.state = EscrowState::Active;
Ok(())
}

View File

@@ -26,13 +26,13 @@ pub mod descro {
create_escrow::handler(ctx, amount, dispute_resolver, escrow_id) create_escrow::handler(ctx, amount, dispute_resolver, escrow_id)
} }
pub fn create_escrow_prefunded( pub fn buyer_create_escrow(
ctx: Context<CreateEscrowPrefunded>, ctx: Context<BuyerCreateEscrow>,
amount: u64, amount: u64,
dispute_resolver: Option<Pubkey>, dispute_resolver: Option<Pubkey>,
escrow_id: u64, escrow_id: u64,
) -> Result<()> { ) -> Result<()> {
create_escrow_prefunded::handler(ctx, amount, dispute_resolver, escrow_id) buyer_create_escrow::handler(ctx, amount, dispute_resolver, escrow_id)
} }
pub fn deposit(ctx: Context<Deposit>) -> Result<()> { pub fn deposit(ctx: Context<Deposit>) -> Result<()> {
@@ -51,6 +51,10 @@ pub mod descro {
resolve::handler(ctx, winner) resolve::handler(ctx, winner)
} }
pub fn seller_confirm(ctx: Context<SellerConfirm>) -> Result<()> {
seller_confirm::handler(ctx)
}
pub fn cancel(ctx: Context<Cancel>) -> Result<()> { pub fn cancel(ctx: Context<Cancel>) -> Result<()> {
cancel::handler(ctx) cancel::handler(ctx)
} }

View File

@@ -17,6 +17,7 @@ pub struct EscrowAccount {
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, InitSpace, Debug)] #[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, InitSpace, Debug)]
pub enum EscrowState { pub enum EscrowState {
AwaitingDeposit, AwaitingDeposit,
AwaitingSellerConfirm,
Active, Active,
Disputed, Disputed,
Complete, Complete,

View File

@@ -81,6 +81,24 @@ pub fn ix_create_escrow(
amount: u64, amount: u64,
dispute_resolver: Option<Pubkey>, dispute_resolver: Option<Pubkey>,
escrow_id: u64, escrow_id: u64,
) -> Instruction {
// Default: derive the resolver_entry PDA from the resolver (won't exist → empty account → policy check skipped)
let dummy = Pubkey::new_from_array([1u8; 32]);
let (resolver, resolver_entry) = match dispute_resolver {
Some(r) => (r, resolver_entry_pda(&r)),
None => (dummy, dummy),
};
ix_create_escrow_with_resolver(seller, buyer, amount, dispute_resolver, escrow_id, &resolver, &resolver_entry)
}
pub fn ix_create_escrow_with_resolver(
seller: &Pubkey,
buyer: &Pubkey,
amount: u64,
dispute_resolver: Option<Pubkey>,
escrow_id: u64,
resolver: &Pubkey,
resolver_entry: &Pubkey,
) -> Instruction { ) -> Instruction {
let escrow = escrow_pda(seller, escrow_id); let escrow = escrow_pda(seller, escrow_id);
let vault = vault_pda(&escrow); let vault = vault_pda(&escrow);
@@ -95,8 +113,10 @@ pub fn ix_create_escrow(
descro::accounts::CreateEscrow { descro::accounts::CreateEscrow {
seller: *seller, seller: *seller,
buyer: *buyer, buyer: *buyer,
resolver: *resolver,
escrow_account: escrow, escrow_account: escrow,
vault, vault,
resolver_entry: *resolver_entry,
system_program: system_program::ID, system_program: system_program::ID,
} }
.to_account_metas(None), .to_account_metas(None),
@@ -182,20 +202,66 @@ pub fn ix_resolve(
) )
} }
pub fn ix_cancel(seller: &Pubkey, escrow_id: u64) -> Instruction { pub fn ix_cancel(canceller: &Pubkey, buyer: &Pubkey, seller: &Pubkey, escrow_id: u64) -> Instruction {
let escrow = escrow_pda(seller, escrow_id); let escrow = escrow_pda(seller, escrow_id);
let vault = vault_pda(&escrow);
Instruction::new_with_bytes( Instruction::new_with_bytes(
descro::id(), descro::id(),
&descro::instruction::Cancel {}.data(), &descro::instruction::Cancel {}.data(),
descro::accounts::Cancel { descro::accounts::Cancel {
canceller: *canceller,
buyer: *buyer,
escrow_account: escrow,
vault,
system_program: system_program::ID,
}
.to_account_metas(None),
)
}
pub fn ix_buyer_create_escrow(
buyer: &Pubkey,
seller: &Pubkey,
amount: u64,
dispute_resolver: Option<Pubkey>,
escrow_id: u64,
) -> Instruction {
let escrow = escrow_pda(seller, escrow_id);
let vault = vault_pda(&escrow);
Instruction::new_with_bytes(
descro::id(),
&descro::instruction::BuyerCreateEscrow {
amount,
dispute_resolver,
escrow_id,
}
.data(),
descro::accounts::BuyerCreateEscrow {
buyer: *buyer,
seller: *seller, seller: *seller,
escrow_account: escrow, escrow_account: escrow,
vault,
system_program: system_program::ID, system_program: system_program::ID,
} }
.to_account_metas(None), .to_account_metas(None),
) )
} }
pub fn ix_seller_confirm(seller: &Pubkey, resolver: &Pubkey, resolver_entry: &Pubkey, escrow_id: u64) -> Instruction {
let escrow = escrow_pda(seller, escrow_id);
Instruction::new_with_bytes(
descro::id(),
&descro::instruction::SellerConfirm {}.data(),
descro::accounts::SellerConfirm {
seller: *seller,
resolver: *resolver,
escrow_account: escrow,
resolver_entry: *resolver_entry,
}
.to_account_metas(None),
)
}
pub fn read_escrow(svm: &LiteSVM, seller: &Pubkey, escrow_id: u64) -> EscrowAccount { pub fn read_escrow(svm: &LiteSVM, seller: &Pubkey, escrow_id: u64) -> EscrowAccount {
let escrow = escrow_pda(seller, escrow_id); let escrow = escrow_pda(seller, escrow_id);
let account = svm.get_account(&escrow).expect("escrow account not found"); let account = svm.get_account(&escrow).expect("escrow account not found");
@@ -247,30 +313,3 @@ pub fn ix_emergency_resolve(
) )
} }
pub fn ix_create_escrow_prefunded(
seller: &Pubkey,
buyer: &Pubkey,
amount: u64,
dispute_resolver: Option<Pubkey>,
escrow_id: u64,
) -> Instruction {
let escrow = escrow_pda(seller, escrow_id);
let vault = vault_pda(&escrow);
Instruction::new_with_bytes(
descro::id(),
&descro::instruction::CreateEscrowPrefunded {
amount,
dispute_resolver,
escrow_id,
}
.data(),
descro::accounts::CreateEscrowPrefunded {
seller: *seller,
buyer: *buyer,
escrow_account: escrow,
vault,
system_program: system_program::ID,
}
.to_account_metas(None),
)
}

View File

@@ -0,0 +1,74 @@
mod common;
use common::*;
use anchor_lang::solana_program::system_program;
use solana_signer::Signer;
#[test]
fn buyer_can_create_and_fund_escrow() {
let (mut svm, seller, buyer, _resolver) = setup();
send(
&mut svm,
ix_buyer_create_escrow(&buyer.pubkey(), &seller.pubkey(), AMOUNT, None, ESCROW_ID),
&buyer,
);
let escrow = read_escrow(&svm, &seller.pubkey(), ESCROW_ID);
assert_eq!(escrow.state, EscrowState::AwaitingSellerConfirm);
assert_eq!(escrow.buyer, buyer.pubkey());
assert_eq!(escrow.seller, seller.pubkey());
assert_eq!(escrow.amount, AMOUNT);
let vault = vault_pda(&escrow_pda(&seller.pubkey(), ESCROW_ID));
let vault_lamports = svm.get_account(&vault).map(|a| a.lamports).unwrap_or(0);
assert_eq!(vault_lamports, AMOUNT);
}
#[test]
fn buyer_create_escrow_fails_below_rent_minimum() {
let (mut svm, seller, buyer, _resolver) = setup();
assert!(!try_send(
&mut svm,
ix_buyer_create_escrow(&buyer.pubkey(), &seller.pubkey(), 0, None, ESCROW_ID),
&buyer,
));
}
#[test]
fn seller_can_confirm_buyer_escrow() {
let (mut svm, seller, buyer, _resolver) = setup();
send(
&mut svm,
ix_buyer_create_escrow(&buyer.pubkey(), &seller.pubkey(), AMOUNT, None, ESCROW_ID),
&buyer,
);
send(
&mut svm,
ix_seller_confirm(&seller.pubkey(), &seller.pubkey(), &system_program::ID, ESCROW_ID),
&seller,
);
let escrow = read_escrow(&svm, &seller.pubkey(), ESCROW_ID);
assert_eq!(escrow.state, EscrowState::Active);
}
#[test]
fn seller_confirm_fails_from_wrong_state() {
let (mut svm, seller, buyer, _resolver) = setup();
// Seller-initiated → AwaitingDeposit — seller_confirm must fail
send(
&mut svm,
ix_create_escrow(&seller.pubkey(), &buyer.pubkey(), AMOUNT, None, ESCROW_ID),
&seller,
);
assert!(!try_send(
&mut svm,
ix_seller_confirm(&seller.pubkey(), &seller.pubkey(), &system_program::ID, ESCROW_ID),
&seller,
));
}

View File

@@ -3,7 +3,7 @@ use common::*;
use solana_signer::Signer; use solana_signer::Signer;
#[test] #[test]
fn cancel_returns_rent_to_seller() { fn seller_can_cancel_awaiting_deposit() {
let (mut svm, seller, buyer, _resolver) = setup(); let (mut svm, seller, buyer, _resolver) = setup();
send( send(
@@ -14,7 +14,7 @@ fn cancel_returns_rent_to_seller() {
let seller_before = svm.get_account(&seller.pubkey()).map(|a| a.lamports).unwrap_or(0); let seller_before = svm.get_account(&seller.pubkey()).map(|a| a.lamports).unwrap_or(0);
send(&mut svm, ix_cancel(&seller.pubkey(), ESCROW_ID), &seller); send(&mut svm, ix_cancel(&seller.pubkey(), &buyer.pubkey(), &seller.pubkey(), ESCROW_ID), &seller);
assert!(svm.get_account(&escrow_pda(&seller.pubkey(), ESCROW_ID)).is_none()); assert!(svm.get_account(&escrow_pda(&seller.pubkey(), ESCROW_ID)).is_none());
let seller_after = svm.get_account(&seller.pubkey()).map(|a| a.lamports).unwrap_or(0); let seller_after = svm.get_account(&seller.pubkey()).map(|a| a.lamports).unwrap_or(0);
@@ -22,7 +22,60 @@ fn cancel_returns_rent_to_seller() {
} }
#[test] #[test]
fn cancel_after_deposit_fails() { fn buyer_can_cancel_awaiting_deposit() {
let (mut svm, seller, buyer, _resolver) = setup();
send(
&mut svm,
ix_create_escrow(&seller.pubkey(), &buyer.pubkey(), AMOUNT, None, ESCROW_ID),
&seller,
);
send(&mut svm, ix_cancel(&buyer.pubkey(), &buyer.pubkey(), &seller.pubkey(), ESCROW_ID), &buyer);
assert!(svm.get_account(&escrow_pda(&seller.pubkey(), ESCROW_ID)).is_none());
}
#[test]
fn seller_can_cancel_awaiting_seller_confirm() {
let (mut svm, seller, buyer, _resolver) = setup();
send(
&mut svm,
ix_buyer_create_escrow(&buyer.pubkey(), &seller.pubkey(), AMOUNT, None, ESCROW_ID),
&buyer,
);
let buyer_before = svm.get_account(&buyer.pubkey()).map(|a| a.lamports).unwrap_or(0);
send(&mut svm, ix_cancel(&seller.pubkey(), &buyer.pubkey(), &seller.pubkey(), ESCROW_ID), &seller);
assert!(svm.get_account(&escrow_pda(&seller.pubkey(), ESCROW_ID)).is_none());
let buyer_after = svm.get_account(&buyer.pubkey()).map(|a| a.lamports).unwrap_or(0);
assert!(buyer_after > buyer_before, "buyer should have received vault refund");
}
#[test]
fn buyer_can_cancel_awaiting_seller_confirm() {
let (mut svm, seller, buyer, _resolver) = setup();
send(
&mut svm,
ix_buyer_create_escrow(&buyer.pubkey(), &seller.pubkey(), AMOUNT, None, ESCROW_ID),
&buyer,
);
let buyer_before = svm.get_account(&buyer.pubkey()).map(|a| a.lamports).unwrap_or(0);
send(&mut svm, ix_cancel(&buyer.pubkey(), &buyer.pubkey(), &seller.pubkey(), ESCROW_ID), &buyer);
assert!(svm.get_account(&escrow_pda(&seller.pubkey(), ESCROW_ID)).is_none());
let buyer_after = svm.get_account(&buyer.pubkey()).map(|a| a.lamports).unwrap_or(0);
assert!(buyer_after > buyer_before, "buyer should have received vault refund");
}
#[test]
fn cannot_cancel_active_escrow() {
let (mut svm, seller, buyer, _resolver) = setup(); let (mut svm, seller, buyer, _resolver) = setup();
send( send(
@@ -34,7 +87,7 @@ fn cancel_after_deposit_fails() {
assert!(!try_send( assert!(!try_send(
&mut svm, &mut svm,
ix_cancel(&seller.pubkey(), ESCROW_ID), ix_cancel(&seller.pubkey(), &buyer.pubkey(), &seller.pubkey(), ESCROW_ID),
&seller, &seller,
)); ));
} }

View File

@@ -1,107 +0,0 @@
mod common;
use common::*;
use solana_signer::Signer;
#[test]
fn prefunded_creates_active_escrow() {
let (mut svm, seller, buyer, resolver) = setup();
// Pre-fund the vault PDA before the instruction
let escrow = escrow_pda(&seller.pubkey(), ESCROW_ID);
let vault = vault_pda(&escrow);
svm.airdrop(&vault, AMOUNT).unwrap();
send(
&mut svm,
ix_create_escrow_prefunded(
&seller.pubkey(),
&buyer.pubkey(),
AMOUNT,
Some(resolver.pubkey()),
ESCROW_ID,
),
&seller,
);
let escrow_account = read_escrow(&svm, &seller.pubkey(), ESCROW_ID);
assert_eq!(escrow_account.state, EscrowState::Active);
assert_eq!(escrow_account.seller, seller.pubkey());
assert_eq!(escrow_account.buyer, buyer.pubkey());
assert_eq!(escrow_account.amount, AMOUNT);
assert_eq!(escrow_account.dispute_raised_at, None);
}
#[test]
fn prefunded_fails_if_vault_underfunded() {
let (mut svm, seller, buyer, resolver) = setup();
// Fund vault with less than AMOUNT
let escrow = escrow_pda(&seller.pubkey(), ESCROW_ID);
let vault = vault_pda(&escrow);
svm.airdrop(&vault, AMOUNT - 1).unwrap();
assert!(!try_send(
&mut svm,
ix_create_escrow_prefunded(
&seller.pubkey(),
&buyer.pubkey(),
AMOUNT,
Some(resolver.pubkey()),
ESCROW_ID,
),
&seller,
));
}
#[test]
fn prefunded_without_resolver_still_works() {
let (mut svm, seller, buyer, _resolver) = setup();
let escrow = escrow_pda(&seller.pubkey(), ESCROW_ID);
let vault = vault_pda(&escrow);
svm.airdrop(&vault, AMOUNT).unwrap();
send(
&mut svm,
ix_create_escrow_prefunded(
&seller.pubkey(),
&buyer.pubkey(),
AMOUNT,
None,
ESCROW_ID,
),
&seller,
);
let escrow_account = read_escrow(&svm, &seller.pubkey(), ESCROW_ID);
assert_eq!(escrow_account.state, EscrowState::Active);
assert_eq!(escrow_account.dispute_resolver, None);
}
#[test]
fn prefunded_escrow_can_be_completed() {
let (mut svm, seller, buyer, _resolver) = setup();
let escrow = escrow_pda(&seller.pubkey(), ESCROW_ID);
let vault = vault_pda(&escrow);
svm.airdrop(&vault, AMOUNT).unwrap();
send(
&mut svm,
ix_create_escrow_prefunded(
&seller.pubkey(),
&buyer.pubkey(),
AMOUNT,
None,
ESCROW_ID,
),
&seller,
);
let seller_before = svm.get_account(&seller.pubkey()).map(|a| a.lamports).unwrap_or(0);
send(&mut svm, ix_complete(&buyer.pubkey(), &seller.pubkey(), ESCROW_ID), &buyer);
let seller_after = svm.get_account(&seller.pubkey()).map(|a| a.lamports).unwrap_or(0);
assert!(seller_after > seller_before);
assert!(svm.get_account(&escrow_pda(&seller.pubkey(), ESCROW_ID)).is_none());
}

View File

@@ -5,7 +5,7 @@ use common::*;
use anchor_lang::InstructionData; use anchor_lang::InstructionData;
use anchor_lang::ToAccountMetas; use anchor_lang::ToAccountMetas;
use anchor_lang::solana_program::{instruction::Instruction, system_program}; use anchor_lang::solana_program::{instruction::Instruction, system_program};
use descro_ext_resolvers::{ResolverType, ResolverEntry}; use descro_ext_resolvers::{AcceptancePolicy, ResolverType, ResolverEntry};
use anchor_lang::AccountDeserialize; use anchor_lang::AccountDeserialize;
use litesvm::LiteSVM; use litesvm::LiteSVM;
use solana_keypair::Keypair; use solana_keypair::Keypair;
@@ -37,6 +37,7 @@ fn ix_register_resolver(authority: &Pubkey) -> Instruction {
descro_ext_resolvers::id(), descro_ext_resolvers::id(),
&descro_ext_resolvers::instruction::RegisterResolver { &descro_ext_resolvers::instruction::RegisterResolver {
resolver_type: ResolverType::CentralAuthority, resolver_type: ResolverType::CentralAuthority,
acceptance_policy: AcceptancePolicy::Open,
name: "Test Resolver".to_string(), name: "Test Resolver".to_string(),
description: "Integration test resolver".to_string(), description: "Integration test resolver".to_string(),
fee_bps: 100, fee_bps: 100,

View File

@@ -1,5 +1,5 @@
use anchor_lang::prelude::*; use anchor_lang::prelude::*;
use crate::state::{ResolverEntry, ResolverType}; use crate::state::{AcceptancePolicy, ResolverEntry, ResolverType};
use crate::error::RegistryError; use crate::error::RegistryError;
#[derive(Accounts)] #[derive(Accounts)]
@@ -22,6 +22,7 @@ pub struct RegisterResolver<'info> {
pub fn handler( pub fn handler(
ctx: Context<RegisterResolver>, ctx: Context<RegisterResolver>,
resolver_type: ResolverType, resolver_type: ResolverType,
acceptance_policy: AcceptancePolicy,
name: String, name: String,
description: String, description: String,
fee_bps: u16, fee_bps: u16,
@@ -34,6 +35,7 @@ pub fn handler(
let entry = &mut ctx.accounts.resolver_entry; let entry = &mut ctx.accounts.resolver_entry;
entry.authority = ctx.accounts.authority.key(); entry.authority = ctx.accounts.authority.key();
entry.resolver_type = resolver_type; entry.resolver_type = resolver_type;
entry.acceptance_policy = acceptance_policy;
entry.name = name; entry.name = name;
entry.description = description; entry.description = description;
entry.fee_bps = fee_bps; entry.fee_bps = fee_bps;

View File

@@ -1,4 +1,5 @@
#![allow(clippy::diverging_sub_expression)] #![allow(clippy::diverging_sub_expression)]
#![allow(clippy::too_many_arguments)]
pub mod error; pub mod error;
pub mod instructions; pub mod instructions;
@@ -23,6 +24,7 @@ pub mod descro_ext_resolvers {
pub fn register_resolver( pub fn register_resolver(
ctx: Context<RegisterResolver>, ctx: Context<RegisterResolver>,
resolver_type: ResolverType, resolver_type: ResolverType,
acceptance_policy: AcceptancePolicy,
name: String, name: String,
description: String, description: String,
fee_bps: u16, fee_bps: u16,
@@ -32,6 +34,7 @@ pub mod descro_ext_resolvers {
register::handler( register::handler(
ctx, ctx,
resolver_type, resolver_type,
acceptance_policy,
name, name,
description, description,
fee_bps, fee_bps,

View File

@@ -5,6 +5,7 @@ use anchor_lang::prelude::*;
pub struct ResolverEntry { pub struct ResolverEntry {
pub authority: Pubkey, pub authority: Pubkey,
pub resolver_type: ResolverType, pub resolver_type: ResolverType,
pub acceptance_policy: AcceptancePolicy,
#[max_len(64)] #[max_len(64)]
pub name: String, pub name: String,
#[max_len(256)] #[max_len(256)]
@@ -28,6 +29,13 @@ pub enum ResolverType {
Multisig, Multisig,
} }
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, InitSpace, Debug)]
pub enum AcceptancePolicy {
Open,
SignatureGated,
ProgramGated,
}
/// Passed to update_stats; mirrors the Escrow program's Winner enum. /// Passed to update_stats; mirrors the Escrow program's Winner enum.
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq)] #[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq)]
pub enum Ruling { pub enum Ruling {

View File

@@ -1,4 +1,4 @@
#![allow(dead_code, unused_imports)] #![allow(dead_code, unused_imports, clippy::too_many_arguments)]
pub use anchor_lang::prelude::Pubkey; pub use anchor_lang::prelude::Pubkey;
use { use {
@@ -6,7 +6,7 @@ use {
solana_program::{instruction::Instruction, system_program}, solana_program::{instruction::Instruction, system_program},
AccountDeserialize, InstructionData, ToAccountMetas, AccountDeserialize, InstructionData, ToAccountMetas,
}, },
descro_ext_resolvers::{ResolverEntry, ResolverType}, descro_ext_resolvers::{AcceptancePolicy, ResolverEntry, ResolverType},
litesvm::LiteSVM, litesvm::LiteSVM,
solana_keypair::Keypair, solana_keypair::Keypair,
solana_message::{Message, VersionedMessage}, solana_message::{Message, VersionedMessage},
@@ -51,6 +51,7 @@ pub fn try_send(svm: &mut LiteSVM, ix: Instruction, payer: &Keypair) -> bool {
pub fn ix_register( pub fn ix_register(
authority: &Pubkey, authority: &Pubkey,
resolver_type: ResolverType, resolver_type: ResolverType,
acceptance_policy: AcceptancePolicy,
name: &str, name: &str,
description: &str, description: &str,
fee_bps: u16, fee_bps: u16,
@@ -62,6 +63,7 @@ pub fn ix_register(
descro_ext_resolvers::id(), descro_ext_resolvers::id(),
&descro_ext_resolvers::instruction::RegisterResolver { &descro_ext_resolvers::instruction::RegisterResolver {
resolver_type, resolver_type,
acceptance_policy,
name: name.to_string(), name: name.to_string(),
description: description.to_string(), description: description.to_string(),
fee_bps, fee_bps,

View File

@@ -1,6 +1,6 @@
mod common; mod common;
use common::*; use common::*;
use descro_ext_resolvers::ResolverType; use descro_ext_resolvers::{AcceptancePolicy, ResolverType};
use solana_keypair::Keypair; use solana_keypair::Keypair;
use solana_signer::Signer; use solana_signer::Signer;
@@ -13,6 +13,7 @@ fn registers_entry_with_correct_state() {
ix_register( ix_register(
&authority.pubkey(), &authority.pubkey(),
ResolverType::CentralAuthority, ResolverType::CentralAuthority,
AcceptancePolicy::Open,
"Acme Resolvers", "Acme Resolvers",
"Fast and fair dispute resolution", "Fast and fair dispute resolution",
100, // 1% 100, // 1%
@@ -25,6 +26,7 @@ fn registers_entry_with_correct_state() {
let entry = read_entry(&svm, &authority.pubkey()); let entry = read_entry(&svm, &authority.pubkey());
assert_eq!(entry.authority, authority.pubkey()); assert_eq!(entry.authority, authority.pubkey());
assert_eq!(entry.resolver_type, ResolverType::CentralAuthority); assert_eq!(entry.resolver_type, ResolverType::CentralAuthority);
assert_eq!(entry.acceptance_policy, AcceptancePolicy::Open);
assert_eq!(entry.name, "Acme Resolvers"); assert_eq!(entry.name, "Acme Resolvers");
assert_eq!(entry.fee_bps, 100); assert_eq!(entry.fee_bps, 100);
assert_eq!(entry.total_resolved, 0); assert_eq!(entry.total_resolved, 0);
@@ -41,6 +43,7 @@ fn duplicate_register_fails() {
ix_register( ix_register(
&authority.pubkey(), &authority.pubkey(),
ResolverType::CentralAuthority, ResolverType::CentralAuthority,
AcceptancePolicy::Open,
"Acme", "Acme",
"", "",
0, 0,
@@ -54,6 +57,7 @@ fn duplicate_register_fails() {
ix_register( ix_register(
&authority.pubkey(), &authority.pubkey(),
ResolverType::CentralAuthority, ResolverType::CentralAuthority,
AcceptancePolicy::Open,
"Acme Again", "Acme Again",
"", "",
0, 0,
@@ -73,6 +77,7 @@ fn register_empty_name_fails() {
ix_register( ix_register(
&authority.pubkey(), &authority.pubkey(),
ResolverType::CentralAuthority, ResolverType::CentralAuthority,
AcceptancePolicy::Open,
"", "",
"", "",
0, 0,
@@ -92,6 +97,7 @@ fn register_invalid_fee_bps_fails() {
ix_register( ix_register(
&authority.pubkey(), &authority.pubkey(),
ResolverType::CentralAuthority, ResolverType::CentralAuthority,
AcceptancePolicy::Open,
"Acme", "Acme",
"", "",
10_001, // > 100% 10_001, // > 100%
@@ -110,12 +116,12 @@ fn different_authorities_have_separate_entries() {
send( send(
&mut svm, &mut svm,
ix_register(&auth1.pubkey(), ResolverType::CentralAuthority, "Auth1", "", 0, auth1.pubkey(), ""), ix_register(&auth1.pubkey(), ResolverType::CentralAuthority, AcceptancePolicy::Open, "Auth1", "", 0, auth1.pubkey(), ""),
&auth1, &auth1,
); );
send( send(
&mut svm, &mut svm,
ix_register(&auth2.pubkey(), ResolverType::JuryDAO, "Auth2", "", 50, auth2.pubkey(), ""), ix_register(&auth2.pubkey(), ResolverType::JuryDAO, AcceptancePolicy::Open, "Auth2", "", 50, auth2.pubkey(), ""),
&auth2, &auth2,
); );

View File

@@ -1,6 +1,6 @@
mod common; mod common;
use common::*; use common::*;
use descro_ext_resolvers::ResolverType; use descro_ext_resolvers::{AcceptancePolicy, ResolverType};
use solana_keypair::Keypair; use solana_keypair::Keypair;
use solana_signer::Signer; use solana_signer::Signer;
@@ -10,6 +10,7 @@ fn register_default(svm: &mut litesvm::LiteSVM, authority: &Keypair) {
ix_register( ix_register(
&authority.pubkey(), &authority.pubkey(),
ResolverType::CentralAuthority, ResolverType::CentralAuthority,
AcceptancePolicy::Open,
"Original Name", "Original Name",
"Original description", "Original description",
100, 100,

View File

@@ -19,6 +19,8 @@ import {
getAddressEncoder, getAddressEncoder,
getBytesDecoder, getBytesDecoder,
getBytesEncoder, getBytesEncoder,
getI64Decoder,
getI64Encoder,
getOptionDecoder, getOptionDecoder,
getOptionEncoder, getOptionEncoder,
getStructDecoder, getStructDecoder,
@@ -69,6 +71,7 @@ export type EscrowAccount = {
bump: number; bump: number;
vaultBump: number; vaultBump: number;
escrowId: bigint; escrowId: bigint;
disputeRaisedAt: Option<bigint>;
}; };
export type EscrowAccountArgs = { export type EscrowAccountArgs = {
@@ -80,6 +83,7 @@ export type EscrowAccountArgs = {
bump: number; bump: number;
vaultBump: number; vaultBump: number;
escrowId: number | bigint; escrowId: number | bigint;
disputeRaisedAt: OptionOrNullable<number | bigint>;
}; };
/** Gets the encoder for {@link EscrowAccountArgs} account data. */ /** Gets the encoder for {@link EscrowAccountArgs} account data. */
@@ -95,6 +99,7 @@ export function getEscrowAccountEncoder(): Encoder<EscrowAccountArgs> {
["bump", getU8Encoder()], ["bump", getU8Encoder()],
["vaultBump", getU8Encoder()], ["vaultBump", getU8Encoder()],
["escrowId", getU64Encoder()], ["escrowId", getU64Encoder()],
["disputeRaisedAt", getOptionEncoder(getI64Encoder())],
]), ]),
(value) => ({ ...value, discriminator: ESCROW_ACCOUNT_DISCRIMINATOR }), (value) => ({ ...value, discriminator: ESCROW_ACCOUNT_DISCRIMINATOR }),
); );
@@ -112,6 +117,7 @@ export function getEscrowAccountDecoder(): Decoder<EscrowAccount> {
["bump", getU8Decoder()], ["bump", getU8Decoder()],
["vaultBump", getU8Decoder()], ["vaultBump", getU8Decoder()],
["escrowId", getU64Decoder()], ["escrowId", getU64Decoder()],
["disputeRaisedAt", getOptionDecoder(getI64Decoder())],
]); ]);
} }

View File

@@ -24,20 +24,36 @@ export const DESCRO_ERROR__NO_RESOLVER_CONFIGURED = 0x1772; // 6002
export const DESCRO_ERROR__UNAUTHORIZED_RESOLVER = 0x1773; // 6003 export const DESCRO_ERROR__UNAUTHORIZED_RESOLVER = 0x1773; // 6003
/** Expired: Escrow has expired */ /** Expired: Escrow has expired */
export const DESCRO_ERROR__EXPIRED = 0x1774; // 6004 export const DESCRO_ERROR__EXPIRED = 0x1774; // 6004
/** DisputeTimeoutNotReached: Dispute timeout period has not elapsed yet */
export const DESCRO_ERROR__DISPUTE_TIMEOUT_NOT_REACHED = 0x1775; // 6005
/** InsufficientVaultBalance: Vault balance is insufficient for the requested escrow amount */
export const DESCRO_ERROR__INSUFFICIENT_VAULT_BALANCE = 0x1776; // 6006
/** AmountBelowRentMinimum: Amount must be at least rent-exempt minimum for the vault (~890880 lamports) */
export const DESCRO_ERROR__AMOUNT_BELOW_RENT_MINIMUM = 0x1777; // 6007
/** ResolverSignatureRequired: Resolver must co-sign this transaction (SignatureGated acceptance policy) */
export const DESCRO_ERROR__RESOLVER_SIGNATURE_REQUIRED = 0x1778; // 6008
export type DescroError = export type DescroError =
| typeof DESCRO_ERROR__AMOUNT_BELOW_RENT_MINIMUM
| typeof DESCRO_ERROR__DISPUTE_TIMEOUT_NOT_REACHED
| typeof DESCRO_ERROR__EXPIRED | typeof DESCRO_ERROR__EXPIRED
| typeof DESCRO_ERROR__INSUFFICIENT_VAULT_BALANCE
| typeof DESCRO_ERROR__INVALID_STATE | typeof DESCRO_ERROR__INVALID_STATE
| typeof DESCRO_ERROR__NO_RESOLVER_CONFIGURED | typeof DESCRO_ERROR__NO_RESOLVER_CONFIGURED
| typeof DESCRO_ERROR__RESOLVER_SIGNATURE_REQUIRED
| typeof DESCRO_ERROR__UNAUTHORIZED | typeof DESCRO_ERROR__UNAUTHORIZED
| typeof DESCRO_ERROR__UNAUTHORIZED_RESOLVER; | typeof DESCRO_ERROR__UNAUTHORIZED_RESOLVER;
let descroErrorMessages: Record<DescroError, string> | undefined; let descroErrorMessages: Record<DescroError, string> | undefined;
if (process.env["NODE_ENV"] !== "production") { if (process.env["NODE_ENV"] !== "production") {
descroErrorMessages = { descroErrorMessages = {
[DESCRO_ERROR__AMOUNT_BELOW_RENT_MINIMUM]: `Amount must be at least rent-exempt minimum for the vault (~890880 lamports)`,
[DESCRO_ERROR__DISPUTE_TIMEOUT_NOT_REACHED]: `Dispute timeout period has not elapsed yet`,
[DESCRO_ERROR__EXPIRED]: `Escrow has expired`, [DESCRO_ERROR__EXPIRED]: `Escrow has expired`,
[DESCRO_ERROR__INSUFFICIENT_VAULT_BALANCE]: `Vault balance is insufficient for the requested escrow amount`,
[DESCRO_ERROR__INVALID_STATE]: `Invalid state for this instruction`, [DESCRO_ERROR__INVALID_STATE]: `Invalid state for this instruction`,
[DESCRO_ERROR__NO_RESOLVER_CONFIGURED]: `No resolver configured for this escrow`, [DESCRO_ERROR__NO_RESOLVER_CONFIGURED]: `No resolver configured for this escrow`,
[DESCRO_ERROR__RESOLVER_SIGNATURE_REQUIRED]: `Resolver must co-sign this transaction (SignatureGated acceptance policy)`,
[DESCRO_ERROR__UNAUTHORIZED]: `Signer is not authorized`, [DESCRO_ERROR__UNAUTHORIZED]: `Signer is not authorized`,
[DESCRO_ERROR__UNAUTHORIZED_RESOLVER]: `Signer is not the configured resolver`, [DESCRO_ERROR__UNAUTHORIZED_RESOLVER]: `Signer is not the configured resolver`,
}; };

View File

@@ -0,0 +1,384 @@
/**
* This code was AUTOGENERATED using the Codama library.
* Please DO NOT EDIT THIS FILE, instead use visitors
* to add features, then rerun Codama to update it.
*
* @see https://github.com/codama-idl/codama
*/
import {
combineCodec,
fixDecoderSize,
fixEncoderSize,
getAddressDecoder,
getAddressEncoder,
getBytesDecoder,
getBytesEncoder,
getOptionDecoder,
getOptionEncoder,
getStructDecoder,
getStructEncoder,
getU64Decoder,
getU64Encoder,
SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS,
SolanaError,
transformEncoder,
type AccountMeta,
type AccountSignerMeta,
type Address,
type Codec,
type Decoder,
type Encoder,
type Instruction,
type InstructionWithAccounts,
type InstructionWithData,
type Option,
type OptionOrNullable,
type ReadonlyAccount,
type ReadonlyUint8Array,
type TransactionSigner,
type WritableAccount,
type WritableSignerAccount,
} from "@solana/kit";
import {
getAccountMetaFactory,
getAddressFromResolvedInstructionAccount,
getNonNullResolvedInstructionInput,
type ResolvedInstructionAccount,
} from "@solana/program-client-core";
import { findEscrowAccountPda, findVaultPda } from "../pdas";
import { DESCRO_PROGRAM_ADDRESS } from "../programs";
export const BUYER_CREATE_ESCROW_DISCRIMINATOR: ReadonlyUint8Array =
new Uint8Array([79, 199, 251, 30, 163, 86, 13, 73]);
export function getBuyerCreateEscrowDiscriminatorBytes(): ReadonlyUint8Array {
return fixEncoderSize(getBytesEncoder(), 8).encode(
BUYER_CREATE_ESCROW_DISCRIMINATOR,
);
}
export type BuyerCreateEscrowInstruction<
TProgram extends string = typeof DESCRO_PROGRAM_ADDRESS,
TAccountBuyer extends string | AccountMeta<string> = string,
TAccountSeller extends string | AccountMeta<string> = string,
TAccountEscrowAccount extends string | AccountMeta<string> = string,
TAccountVault extends string | AccountMeta<string> = string,
TAccountSystemProgram extends string | AccountMeta<string> =
"11111111111111111111111111111111",
TRemainingAccounts extends readonly AccountMeta<string>[] = [],
> = Instruction<TProgram> &
InstructionWithData<ReadonlyUint8Array> &
InstructionWithAccounts<
[
TAccountBuyer extends string
? WritableSignerAccount<TAccountBuyer> &
AccountSignerMeta<TAccountBuyer>
: TAccountBuyer,
TAccountSeller extends string
? ReadonlyAccount<TAccountSeller>
: TAccountSeller,
TAccountEscrowAccount extends string
? WritableAccount<TAccountEscrowAccount>
: TAccountEscrowAccount,
TAccountVault extends string
? WritableAccount<TAccountVault>
: TAccountVault,
TAccountSystemProgram extends string
? ReadonlyAccount<TAccountSystemProgram>
: TAccountSystemProgram,
...TRemainingAccounts,
]
>;
export type BuyerCreateEscrowInstructionData = {
discriminator: ReadonlyUint8Array;
amount: bigint;
disputeResolver: Option<Address>;
escrowId: bigint;
};
export type BuyerCreateEscrowInstructionDataArgs = {
amount: number | bigint;
disputeResolver: OptionOrNullable<Address>;
escrowId: number | bigint;
};
export function getBuyerCreateEscrowInstructionDataEncoder(): Encoder<BuyerCreateEscrowInstructionDataArgs> {
return transformEncoder(
getStructEncoder([
["discriminator", fixEncoderSize(getBytesEncoder(), 8)],
["amount", getU64Encoder()],
["disputeResolver", getOptionEncoder(getAddressEncoder())],
["escrowId", getU64Encoder()],
]),
(value) => ({ ...value, discriminator: BUYER_CREATE_ESCROW_DISCRIMINATOR }),
);
}
export function getBuyerCreateEscrowInstructionDataDecoder(): Decoder<BuyerCreateEscrowInstructionData> {
return getStructDecoder([
["discriminator", fixDecoderSize(getBytesDecoder(), 8)],
["amount", getU64Decoder()],
["disputeResolver", getOptionDecoder(getAddressDecoder())],
["escrowId", getU64Decoder()],
]);
}
export function getBuyerCreateEscrowInstructionDataCodec(): Codec<
BuyerCreateEscrowInstructionDataArgs,
BuyerCreateEscrowInstructionData
> {
return combineCodec(
getBuyerCreateEscrowInstructionDataEncoder(),
getBuyerCreateEscrowInstructionDataDecoder(),
);
}
export type BuyerCreateEscrowAsyncInput<
TAccountBuyer extends string = string,
TAccountSeller extends string = string,
TAccountEscrowAccount extends string = string,
TAccountVault extends string = string,
TAccountSystemProgram extends string = string,
> = {
buyer: TransactionSigner<TAccountBuyer>;
seller: Address<TAccountSeller>;
escrowAccount?: Address<TAccountEscrowAccount>;
vault?: Address<TAccountVault>;
systemProgram?: Address<TAccountSystemProgram>;
amount: BuyerCreateEscrowInstructionDataArgs["amount"];
disputeResolver: BuyerCreateEscrowInstructionDataArgs["disputeResolver"];
escrowId: BuyerCreateEscrowInstructionDataArgs["escrowId"];
};
export async function getBuyerCreateEscrowInstructionAsync<
TAccountBuyer extends string,
TAccountSeller extends string,
TAccountEscrowAccount extends string,
TAccountVault extends string,
TAccountSystemProgram extends string,
TProgramAddress extends Address = typeof DESCRO_PROGRAM_ADDRESS,
>(
input: BuyerCreateEscrowAsyncInput<
TAccountBuyer,
TAccountSeller,
TAccountEscrowAccount,
TAccountVault,
TAccountSystemProgram
>,
config?: { programAddress?: TProgramAddress },
): Promise<
BuyerCreateEscrowInstruction<
TProgramAddress,
TAccountBuyer,
TAccountSeller,
TAccountEscrowAccount,
TAccountVault,
TAccountSystemProgram
>
> {
// Program address.
const programAddress = config?.programAddress ?? DESCRO_PROGRAM_ADDRESS;
// Original accounts.
const originalAccounts = {
buyer: { value: input.buyer ?? null, isWritable: true },
seller: { value: input.seller ?? null, isWritable: false },
escrowAccount: { value: input.escrowAccount ?? null, isWritable: true },
vault: { value: input.vault ?? null, isWritable: true },
systemProgram: { value: input.systemProgram ?? null, isWritable: false },
};
const accounts = originalAccounts as Record<
keyof typeof originalAccounts,
ResolvedInstructionAccount
>;
// Original args.
const args = { ...input };
// Resolve default values.
if (!accounts.escrowAccount.value) {
accounts.escrowAccount.value = await findEscrowAccountPda({
seller: getAddressFromResolvedInstructionAccount(
"seller",
accounts.seller.value,
),
escrowId: getNonNullResolvedInstructionInput("escrowId", args.escrowId),
});
}
if (!accounts.vault.value) {
accounts.vault.value = await findVaultPda({
escrowAccount: getAddressFromResolvedInstructionAccount(
"escrowAccount",
accounts.escrowAccount.value,
),
});
}
if (!accounts.systemProgram.value) {
accounts.systemProgram.value =
"11111111111111111111111111111111" as Address<"11111111111111111111111111111111">;
}
const getAccountMeta = getAccountMetaFactory(programAddress, "programId");
return Object.freeze({
accounts: [
getAccountMeta("buyer", accounts.buyer),
getAccountMeta("seller", accounts.seller),
getAccountMeta("escrowAccount", accounts.escrowAccount),
getAccountMeta("vault", accounts.vault),
getAccountMeta("systemProgram", accounts.systemProgram),
],
data: getBuyerCreateEscrowInstructionDataEncoder().encode(
args as BuyerCreateEscrowInstructionDataArgs,
),
programAddress,
} as BuyerCreateEscrowInstruction<
TProgramAddress,
TAccountBuyer,
TAccountSeller,
TAccountEscrowAccount,
TAccountVault,
TAccountSystemProgram
>);
}
export type BuyerCreateEscrowInput<
TAccountBuyer extends string = string,
TAccountSeller extends string = string,
TAccountEscrowAccount extends string = string,
TAccountVault extends string = string,
TAccountSystemProgram extends string = string,
> = {
buyer: TransactionSigner<TAccountBuyer>;
seller: Address<TAccountSeller>;
escrowAccount: Address<TAccountEscrowAccount>;
vault: Address<TAccountVault>;
systemProgram?: Address<TAccountSystemProgram>;
amount: BuyerCreateEscrowInstructionDataArgs["amount"];
disputeResolver: BuyerCreateEscrowInstructionDataArgs["disputeResolver"];
escrowId: BuyerCreateEscrowInstructionDataArgs["escrowId"];
};
export function getBuyerCreateEscrowInstruction<
TAccountBuyer extends string,
TAccountSeller extends string,
TAccountEscrowAccount extends string,
TAccountVault extends string,
TAccountSystemProgram extends string,
TProgramAddress extends Address = typeof DESCRO_PROGRAM_ADDRESS,
>(
input: BuyerCreateEscrowInput<
TAccountBuyer,
TAccountSeller,
TAccountEscrowAccount,
TAccountVault,
TAccountSystemProgram
>,
config?: { programAddress?: TProgramAddress },
): BuyerCreateEscrowInstruction<
TProgramAddress,
TAccountBuyer,
TAccountSeller,
TAccountEscrowAccount,
TAccountVault,
TAccountSystemProgram
> {
// Program address.
const programAddress = config?.programAddress ?? DESCRO_PROGRAM_ADDRESS;
// Original accounts.
const originalAccounts = {
buyer: { value: input.buyer ?? null, isWritable: true },
seller: { value: input.seller ?? null, isWritable: false },
escrowAccount: { value: input.escrowAccount ?? null, isWritable: true },
vault: { value: input.vault ?? null, isWritable: true },
systemProgram: { value: input.systemProgram ?? null, isWritable: false },
};
const accounts = originalAccounts as Record<
keyof typeof originalAccounts,
ResolvedInstructionAccount
>;
// Original args.
const args = { ...input };
// Resolve default values.
if (!accounts.systemProgram.value) {
accounts.systemProgram.value =
"11111111111111111111111111111111" as Address<"11111111111111111111111111111111">;
}
const getAccountMeta = getAccountMetaFactory(programAddress, "programId");
return Object.freeze({
accounts: [
getAccountMeta("buyer", accounts.buyer),
getAccountMeta("seller", accounts.seller),
getAccountMeta("escrowAccount", accounts.escrowAccount),
getAccountMeta("vault", accounts.vault),
getAccountMeta("systemProgram", accounts.systemProgram),
],
data: getBuyerCreateEscrowInstructionDataEncoder().encode(
args as BuyerCreateEscrowInstructionDataArgs,
),
programAddress,
} as BuyerCreateEscrowInstruction<
TProgramAddress,
TAccountBuyer,
TAccountSeller,
TAccountEscrowAccount,
TAccountVault,
TAccountSystemProgram
>);
}
export type ParsedBuyerCreateEscrowInstruction<
TProgram extends string = typeof DESCRO_PROGRAM_ADDRESS,
TAccountMetas extends readonly AccountMeta[] = readonly AccountMeta[],
> = {
programAddress: Address<TProgram>;
accounts: {
buyer: TAccountMetas[0];
seller: TAccountMetas[1];
escrowAccount: TAccountMetas[2];
vault: TAccountMetas[3];
systemProgram: TAccountMetas[4];
};
data: BuyerCreateEscrowInstructionData;
};
export function parseBuyerCreateEscrowInstruction<
TProgram extends string,
TAccountMetas extends readonly AccountMeta[],
>(
instruction: Instruction<TProgram> &
InstructionWithAccounts<TAccountMetas> &
InstructionWithData<ReadonlyUint8Array>,
): ParsedBuyerCreateEscrowInstruction<TProgram, TAccountMetas> {
if (instruction.accounts.length < 5) {
throw new SolanaError(
SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS,
{
actualAccountMetas: instruction.accounts.length,
expectedAccountMetas: 5,
},
);
}
let accountIndex = 0;
const getNextAccount = () => {
const accountMeta = (instruction.accounts as TAccountMetas)[accountIndex]!;
accountIndex += 1;
return accountMeta;
};
return {
programAddress: instruction.programAddress,
accounts: {
buyer: getNextAccount(),
seller: getNextAccount(),
escrowAccount: getNextAccount(),
vault: getNextAccount(),
systemProgram: getNextAccount(),
},
data: getBuyerCreateEscrowInstructionDataDecoder().decode(instruction.data),
};
}

View File

@@ -34,8 +34,10 @@ import {
} from "@solana/kit"; } from "@solana/kit";
import { import {
getAccountMetaFactory, getAccountMetaFactory,
getAddressFromResolvedInstructionAccount,
type ResolvedInstructionAccount, type ResolvedInstructionAccount,
} from "@solana/program-client-core"; } from "@solana/program-client-core";
import { findVaultPda } from "../pdas";
import { DESCRO_PROGRAM_ADDRESS } from "../programs"; import { DESCRO_PROGRAM_ADDRESS } from "../programs";
export const CANCEL_DISCRIMINATOR: ReadonlyUint8Array = new Uint8Array([ export const CANCEL_DISCRIMINATOR: ReadonlyUint8Array = new Uint8Array([
@@ -48,8 +50,10 @@ export function getCancelDiscriminatorBytes(): ReadonlyUint8Array {
export type CancelInstruction< export type CancelInstruction<
TProgram extends string = typeof DESCRO_PROGRAM_ADDRESS, TProgram extends string = typeof DESCRO_PROGRAM_ADDRESS,
TAccountSeller extends string | AccountMeta<string> = string, TAccountCanceller extends string | AccountMeta<string> = string,
TAccountBuyer extends string | AccountMeta<string> = string,
TAccountEscrowAccount extends string | AccountMeta<string> = string, TAccountEscrowAccount extends string | AccountMeta<string> = string,
TAccountVault extends string | AccountMeta<string> = string,
TAccountSystemProgram extends string | AccountMeta<string> = TAccountSystemProgram extends string | AccountMeta<string> =
"11111111111111111111111111111111", "11111111111111111111111111111111",
TRemainingAccounts extends readonly AccountMeta<string>[] = [], TRemainingAccounts extends readonly AccountMeta<string>[] = [],
@@ -57,13 +61,19 @@ export type CancelInstruction<
InstructionWithData<ReadonlyUint8Array> & InstructionWithData<ReadonlyUint8Array> &
InstructionWithAccounts< InstructionWithAccounts<
[ [
TAccountSeller extends string TAccountCanceller extends string
? WritableSignerAccount<TAccountSeller> & ? WritableSignerAccount<TAccountCanceller> &
AccountSignerMeta<TAccountSeller> AccountSignerMeta<TAccountCanceller>
: TAccountSeller, : TAccountCanceller,
TAccountBuyer extends string
? WritableAccount<TAccountBuyer>
: TAccountBuyer,
TAccountEscrowAccount extends string TAccountEscrowAccount extends string
? WritableAccount<TAccountEscrowAccount> ? WritableAccount<TAccountEscrowAccount>
: TAccountEscrowAccount, : TAccountEscrowAccount,
TAccountVault extends string
? WritableAccount<TAccountVault>
: TAccountVault,
TAccountSystemProgram extends string TAccountSystemProgram extends string
? ReadonlyAccount<TAccountSystemProgram> ? ReadonlyAccount<TAccountSystemProgram>
: TAccountSystemProgram, : TAccountSystemProgram,
@@ -98,32 +108,133 @@ export function getCancelInstructionDataCodec(): FixedSizeCodec<
); );
} }
export type CancelInput< export type CancelAsyncInput<
TAccountSeller extends string = string, TAccountCanceller extends string = string,
TAccountBuyer extends string = string,
TAccountEscrowAccount extends string = string, TAccountEscrowAccount extends string = string,
TAccountVault extends string = string,
TAccountSystemProgram extends string = string, TAccountSystemProgram extends string = string,
> = { > = {
seller: TransactionSigner<TAccountSeller>; canceller: TransactionSigner<TAccountCanceller>;
buyer: Address<TAccountBuyer>;
escrowAccount: Address<TAccountEscrowAccount>; escrowAccount: Address<TAccountEscrowAccount>;
vault?: Address<TAccountVault>;
systemProgram?: Address<TAccountSystemProgram>;
};
export async function getCancelInstructionAsync<
TAccountCanceller extends string,
TAccountBuyer extends string,
TAccountEscrowAccount extends string,
TAccountVault extends string,
TAccountSystemProgram extends string,
TProgramAddress extends Address = typeof DESCRO_PROGRAM_ADDRESS,
>(
input: CancelAsyncInput<
TAccountCanceller,
TAccountBuyer,
TAccountEscrowAccount,
TAccountVault,
TAccountSystemProgram
>,
config?: { programAddress?: TProgramAddress },
): Promise<
CancelInstruction<
TProgramAddress,
TAccountCanceller,
TAccountBuyer,
TAccountEscrowAccount,
TAccountVault,
TAccountSystemProgram
>
> {
// Program address.
const programAddress = config?.programAddress ?? DESCRO_PROGRAM_ADDRESS;
// Original accounts.
const originalAccounts = {
canceller: { value: input.canceller ?? null, isWritable: true },
buyer: { value: input.buyer ?? null, isWritable: true },
escrowAccount: { value: input.escrowAccount ?? null, isWritable: true },
vault: { value: input.vault ?? null, isWritable: true },
systemProgram: { value: input.systemProgram ?? null, isWritable: false },
};
const accounts = originalAccounts as Record<
keyof typeof originalAccounts,
ResolvedInstructionAccount
>;
// Resolve default values.
if (!accounts.vault.value) {
accounts.vault.value = await findVaultPda({
escrowAccount: getAddressFromResolvedInstructionAccount(
"escrowAccount",
accounts.escrowAccount.value,
),
});
}
if (!accounts.systemProgram.value) {
accounts.systemProgram.value =
"11111111111111111111111111111111" as Address<"11111111111111111111111111111111">;
}
const getAccountMeta = getAccountMetaFactory(programAddress, "programId");
return Object.freeze({
accounts: [
getAccountMeta("canceller", accounts.canceller),
getAccountMeta("buyer", accounts.buyer),
getAccountMeta("escrowAccount", accounts.escrowAccount),
getAccountMeta("vault", accounts.vault),
getAccountMeta("systemProgram", accounts.systemProgram),
],
data: getCancelInstructionDataEncoder().encode({}),
programAddress,
} as CancelInstruction<
TProgramAddress,
TAccountCanceller,
TAccountBuyer,
TAccountEscrowAccount,
TAccountVault,
TAccountSystemProgram
>);
}
export type CancelInput<
TAccountCanceller extends string = string,
TAccountBuyer extends string = string,
TAccountEscrowAccount extends string = string,
TAccountVault extends string = string,
TAccountSystemProgram extends string = string,
> = {
canceller: TransactionSigner<TAccountCanceller>;
buyer: Address<TAccountBuyer>;
escrowAccount: Address<TAccountEscrowAccount>;
vault: Address<TAccountVault>;
systemProgram?: Address<TAccountSystemProgram>; systemProgram?: Address<TAccountSystemProgram>;
}; };
export function getCancelInstruction< export function getCancelInstruction<
TAccountSeller extends string, TAccountCanceller extends string,
TAccountBuyer extends string,
TAccountEscrowAccount extends string, TAccountEscrowAccount extends string,
TAccountVault extends string,
TAccountSystemProgram extends string, TAccountSystemProgram extends string,
TProgramAddress extends Address = typeof DESCRO_PROGRAM_ADDRESS, TProgramAddress extends Address = typeof DESCRO_PROGRAM_ADDRESS,
>( >(
input: CancelInput< input: CancelInput<
TAccountSeller, TAccountCanceller,
TAccountBuyer,
TAccountEscrowAccount, TAccountEscrowAccount,
TAccountVault,
TAccountSystemProgram TAccountSystemProgram
>, >,
config?: { programAddress?: TProgramAddress }, config?: { programAddress?: TProgramAddress },
): CancelInstruction< ): CancelInstruction<
TProgramAddress, TProgramAddress,
TAccountSeller, TAccountCanceller,
TAccountBuyer,
TAccountEscrowAccount, TAccountEscrowAccount,
TAccountVault,
TAccountSystemProgram TAccountSystemProgram
> { > {
// Program address. // Program address.
@@ -131,8 +242,10 @@ export function getCancelInstruction<
// Original accounts. // Original accounts.
const originalAccounts = { const originalAccounts = {
seller: { value: input.seller ?? null, isWritable: true }, canceller: { value: input.canceller ?? null, isWritable: true },
buyer: { value: input.buyer ?? null, isWritable: true },
escrowAccount: { value: input.escrowAccount ?? null, isWritable: true }, escrowAccount: { value: input.escrowAccount ?? null, isWritable: true },
vault: { value: input.vault ?? null, isWritable: true },
systemProgram: { value: input.systemProgram ?? null, isWritable: false }, systemProgram: { value: input.systemProgram ?? null, isWritable: false },
}; };
const accounts = originalAccounts as Record< const accounts = originalAccounts as Record<
@@ -149,16 +262,20 @@ export function getCancelInstruction<
const getAccountMeta = getAccountMetaFactory(programAddress, "programId"); const getAccountMeta = getAccountMetaFactory(programAddress, "programId");
return Object.freeze({ return Object.freeze({
accounts: [ accounts: [
getAccountMeta("seller", accounts.seller), getAccountMeta("canceller", accounts.canceller),
getAccountMeta("buyer", accounts.buyer),
getAccountMeta("escrowAccount", accounts.escrowAccount), getAccountMeta("escrowAccount", accounts.escrowAccount),
getAccountMeta("vault", accounts.vault),
getAccountMeta("systemProgram", accounts.systemProgram), getAccountMeta("systemProgram", accounts.systemProgram),
], ],
data: getCancelInstructionDataEncoder().encode({}), data: getCancelInstructionDataEncoder().encode({}),
programAddress, programAddress,
} as CancelInstruction< } as CancelInstruction<
TProgramAddress, TProgramAddress,
TAccountSeller, TAccountCanceller,
TAccountBuyer,
TAccountEscrowAccount, TAccountEscrowAccount,
TAccountVault,
TAccountSystemProgram TAccountSystemProgram
>); >);
} }
@@ -169,9 +286,11 @@ export type ParsedCancelInstruction<
> = { > = {
programAddress: Address<TProgram>; programAddress: Address<TProgram>;
accounts: { accounts: {
seller: TAccountMetas[0]; canceller: TAccountMetas[0];
escrowAccount: TAccountMetas[1]; buyer: TAccountMetas[1];
systemProgram: TAccountMetas[2]; escrowAccount: TAccountMetas[2];
vault: TAccountMetas[3];
systemProgram: TAccountMetas[4];
}; };
data: CancelInstructionData; data: CancelInstructionData;
}; };
@@ -184,12 +303,12 @@ export function parseCancelInstruction<
InstructionWithAccounts<TAccountMetas> & InstructionWithAccounts<TAccountMetas> &
InstructionWithData<ReadonlyUint8Array>, InstructionWithData<ReadonlyUint8Array>,
): ParsedCancelInstruction<TProgram, TAccountMetas> { ): ParsedCancelInstruction<TProgram, TAccountMetas> {
if (instruction.accounts.length < 3) { if (instruction.accounts.length < 5) {
throw new SolanaError( throw new SolanaError(
SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS, SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS,
{ {
actualAccountMetas: instruction.accounts.length, actualAccountMetas: instruction.accounts.length,
expectedAccountMetas: 3, expectedAccountMetas: 5,
}, },
); );
} }
@@ -202,8 +321,10 @@ export function parseCancelInstruction<
return { return {
programAddress: instruction.programAddress, programAddress: instruction.programAddress,
accounts: { accounts: {
seller: getNextAccount(), canceller: getNextAccount(),
buyer: getNextAccount(),
escrowAccount: getNextAccount(), escrowAccount: getNextAccount(),
vault: getNextAccount(),
systemProgram: getNextAccount(), systemProgram: getNextAccount(),
}, },
data: getCancelInstructionDataDecoder().decode(instruction.data), data: getCancelInstructionDataDecoder().decode(instruction.data),

View File

@@ -63,8 +63,10 @@ export type CreateEscrowInstruction<
TProgram extends string = typeof DESCRO_PROGRAM_ADDRESS, TProgram extends string = typeof DESCRO_PROGRAM_ADDRESS,
TAccountSeller extends string | AccountMeta<string> = string, TAccountSeller extends string | AccountMeta<string> = string,
TAccountBuyer extends string | AccountMeta<string> = string, TAccountBuyer extends string | AccountMeta<string> = string,
TAccountResolver extends string | AccountMeta<string> = string,
TAccountEscrowAccount extends string | AccountMeta<string> = string, TAccountEscrowAccount extends string | AccountMeta<string> = string,
TAccountVault extends string | AccountMeta<string> = string, TAccountVault extends string | AccountMeta<string> = string,
TAccountResolverEntry extends string | AccountMeta<string> = string,
TAccountSystemProgram extends string | AccountMeta<string> = TAccountSystemProgram extends string | AccountMeta<string> =
"11111111111111111111111111111111", "11111111111111111111111111111111",
TRemainingAccounts extends readonly AccountMeta<string>[] = [], TRemainingAccounts extends readonly AccountMeta<string>[] = [],
@@ -79,12 +81,18 @@ export type CreateEscrowInstruction<
TAccountBuyer extends string TAccountBuyer extends string
? ReadonlyAccount<TAccountBuyer> ? ReadonlyAccount<TAccountBuyer>
: TAccountBuyer, : TAccountBuyer,
TAccountResolver extends string
? ReadonlyAccount<TAccountResolver>
: TAccountResolver,
TAccountEscrowAccount extends string TAccountEscrowAccount extends string
? WritableAccount<TAccountEscrowAccount> ? WritableAccount<TAccountEscrowAccount>
: TAccountEscrowAccount, : TAccountEscrowAccount,
TAccountVault extends string TAccountVault extends string
? WritableAccount<TAccountVault> ? WritableAccount<TAccountVault>
: TAccountVault, : TAccountVault,
TAccountResolverEntry extends string
? ReadonlyAccount<TAccountResolverEntry>
: TAccountResolverEntry,
TAccountSystemProgram extends string TAccountSystemProgram extends string
? ReadonlyAccount<TAccountSystemProgram> ? ReadonlyAccount<TAccountSystemProgram>
: TAccountSystemProgram, : TAccountSystemProgram,
@@ -139,14 +147,18 @@ export function getCreateEscrowInstructionDataCodec(): Codec<
export type CreateEscrowAsyncInput< export type CreateEscrowAsyncInput<
TAccountSeller extends string = string, TAccountSeller extends string = string,
TAccountBuyer extends string = string, TAccountBuyer extends string = string,
TAccountResolver extends string = string,
TAccountEscrowAccount extends string = string, TAccountEscrowAccount extends string = string,
TAccountVault extends string = string, TAccountVault extends string = string,
TAccountResolverEntry extends string = string,
TAccountSystemProgram extends string = string, TAccountSystemProgram extends string = string,
> = { > = {
seller: TransactionSigner<TAccountSeller>; seller: TransactionSigner<TAccountSeller>;
buyer: Address<TAccountBuyer>; buyer: Address<TAccountBuyer>;
resolver: Address<TAccountResolver>;
escrowAccount?: Address<TAccountEscrowAccount>; escrowAccount?: Address<TAccountEscrowAccount>;
vault?: Address<TAccountVault>; vault?: Address<TAccountVault>;
resolverEntry: Address<TAccountResolverEntry>;
systemProgram?: Address<TAccountSystemProgram>; systemProgram?: Address<TAccountSystemProgram>;
amount: CreateEscrowInstructionDataArgs["amount"]; amount: CreateEscrowInstructionDataArgs["amount"];
disputeResolver: CreateEscrowInstructionDataArgs["disputeResolver"]; disputeResolver: CreateEscrowInstructionDataArgs["disputeResolver"];
@@ -156,16 +168,20 @@ export type CreateEscrowAsyncInput<
export async function getCreateEscrowInstructionAsync< export async function getCreateEscrowInstructionAsync<
TAccountSeller extends string, TAccountSeller extends string,
TAccountBuyer extends string, TAccountBuyer extends string,
TAccountResolver extends string,
TAccountEscrowAccount extends string, TAccountEscrowAccount extends string,
TAccountVault extends string, TAccountVault extends string,
TAccountResolverEntry extends string,
TAccountSystemProgram extends string, TAccountSystemProgram extends string,
TProgramAddress extends Address = typeof DESCRO_PROGRAM_ADDRESS, TProgramAddress extends Address = typeof DESCRO_PROGRAM_ADDRESS,
>( >(
input: CreateEscrowAsyncInput< input: CreateEscrowAsyncInput<
TAccountSeller, TAccountSeller,
TAccountBuyer, TAccountBuyer,
TAccountResolver,
TAccountEscrowAccount, TAccountEscrowAccount,
TAccountVault, TAccountVault,
TAccountResolverEntry,
TAccountSystemProgram TAccountSystemProgram
>, >,
config?: { programAddress?: TProgramAddress }, config?: { programAddress?: TProgramAddress },
@@ -174,8 +190,10 @@ export async function getCreateEscrowInstructionAsync<
TProgramAddress, TProgramAddress,
TAccountSeller, TAccountSeller,
TAccountBuyer, TAccountBuyer,
TAccountResolver,
TAccountEscrowAccount, TAccountEscrowAccount,
TAccountVault, TAccountVault,
TAccountResolverEntry,
TAccountSystemProgram TAccountSystemProgram
> >
> { > {
@@ -186,8 +204,10 @@ export async function getCreateEscrowInstructionAsync<
const originalAccounts = { const originalAccounts = {
seller: { value: input.seller ?? null, isWritable: true }, seller: { value: input.seller ?? null, isWritable: true },
buyer: { value: input.buyer ?? null, isWritable: false }, buyer: { value: input.buyer ?? null, isWritable: false },
resolver: { value: input.resolver ?? null, isWritable: false },
escrowAccount: { value: input.escrowAccount ?? null, isWritable: true }, escrowAccount: { value: input.escrowAccount ?? null, isWritable: true },
vault: { value: input.vault ?? null, isWritable: true }, vault: { value: input.vault ?? null, isWritable: true },
resolverEntry: { value: input.resolverEntry ?? null, isWritable: false },
systemProgram: { value: input.systemProgram ?? null, isWritable: false }, systemProgram: { value: input.systemProgram ?? null, isWritable: false },
}; };
const accounts = originalAccounts as Record< const accounts = originalAccounts as Record<
@@ -226,8 +246,10 @@ export async function getCreateEscrowInstructionAsync<
accounts: [ accounts: [
getAccountMeta("seller", accounts.seller), getAccountMeta("seller", accounts.seller),
getAccountMeta("buyer", accounts.buyer), getAccountMeta("buyer", accounts.buyer),
getAccountMeta("resolver", accounts.resolver),
getAccountMeta("escrowAccount", accounts.escrowAccount), getAccountMeta("escrowAccount", accounts.escrowAccount),
getAccountMeta("vault", accounts.vault), getAccountMeta("vault", accounts.vault),
getAccountMeta("resolverEntry", accounts.resolverEntry),
getAccountMeta("systemProgram", accounts.systemProgram), getAccountMeta("systemProgram", accounts.systemProgram),
], ],
data: getCreateEscrowInstructionDataEncoder().encode( data: getCreateEscrowInstructionDataEncoder().encode(
@@ -238,8 +260,10 @@ export async function getCreateEscrowInstructionAsync<
TProgramAddress, TProgramAddress,
TAccountSeller, TAccountSeller,
TAccountBuyer, TAccountBuyer,
TAccountResolver,
TAccountEscrowAccount, TAccountEscrowAccount,
TAccountVault, TAccountVault,
TAccountResolverEntry,
TAccountSystemProgram TAccountSystemProgram
>); >);
} }
@@ -247,14 +271,18 @@ export async function getCreateEscrowInstructionAsync<
export type CreateEscrowInput< export type CreateEscrowInput<
TAccountSeller extends string = string, TAccountSeller extends string = string,
TAccountBuyer extends string = string, TAccountBuyer extends string = string,
TAccountResolver extends string = string,
TAccountEscrowAccount extends string = string, TAccountEscrowAccount extends string = string,
TAccountVault extends string = string, TAccountVault extends string = string,
TAccountResolverEntry extends string = string,
TAccountSystemProgram extends string = string, TAccountSystemProgram extends string = string,
> = { > = {
seller: TransactionSigner<TAccountSeller>; seller: TransactionSigner<TAccountSeller>;
buyer: Address<TAccountBuyer>; buyer: Address<TAccountBuyer>;
resolver: Address<TAccountResolver>;
escrowAccount: Address<TAccountEscrowAccount>; escrowAccount: Address<TAccountEscrowAccount>;
vault: Address<TAccountVault>; vault: Address<TAccountVault>;
resolverEntry: Address<TAccountResolverEntry>;
systemProgram?: Address<TAccountSystemProgram>; systemProgram?: Address<TAccountSystemProgram>;
amount: CreateEscrowInstructionDataArgs["amount"]; amount: CreateEscrowInstructionDataArgs["amount"];
disputeResolver: CreateEscrowInstructionDataArgs["disputeResolver"]; disputeResolver: CreateEscrowInstructionDataArgs["disputeResolver"];
@@ -264,16 +292,20 @@ export type CreateEscrowInput<
export function getCreateEscrowInstruction< export function getCreateEscrowInstruction<
TAccountSeller extends string, TAccountSeller extends string,
TAccountBuyer extends string, TAccountBuyer extends string,
TAccountResolver extends string,
TAccountEscrowAccount extends string, TAccountEscrowAccount extends string,
TAccountVault extends string, TAccountVault extends string,
TAccountResolverEntry extends string,
TAccountSystemProgram extends string, TAccountSystemProgram extends string,
TProgramAddress extends Address = typeof DESCRO_PROGRAM_ADDRESS, TProgramAddress extends Address = typeof DESCRO_PROGRAM_ADDRESS,
>( >(
input: CreateEscrowInput< input: CreateEscrowInput<
TAccountSeller, TAccountSeller,
TAccountBuyer, TAccountBuyer,
TAccountResolver,
TAccountEscrowAccount, TAccountEscrowAccount,
TAccountVault, TAccountVault,
TAccountResolverEntry,
TAccountSystemProgram TAccountSystemProgram
>, >,
config?: { programAddress?: TProgramAddress }, config?: { programAddress?: TProgramAddress },
@@ -281,8 +313,10 @@ export function getCreateEscrowInstruction<
TProgramAddress, TProgramAddress,
TAccountSeller, TAccountSeller,
TAccountBuyer, TAccountBuyer,
TAccountResolver,
TAccountEscrowAccount, TAccountEscrowAccount,
TAccountVault, TAccountVault,
TAccountResolverEntry,
TAccountSystemProgram TAccountSystemProgram
> { > {
// Program address. // Program address.
@@ -292,8 +326,10 @@ export function getCreateEscrowInstruction<
const originalAccounts = { const originalAccounts = {
seller: { value: input.seller ?? null, isWritable: true }, seller: { value: input.seller ?? null, isWritable: true },
buyer: { value: input.buyer ?? null, isWritable: false }, buyer: { value: input.buyer ?? null, isWritable: false },
resolver: { value: input.resolver ?? null, isWritable: false },
escrowAccount: { value: input.escrowAccount ?? null, isWritable: true }, escrowAccount: { value: input.escrowAccount ?? null, isWritable: true },
vault: { value: input.vault ?? null, isWritable: true }, vault: { value: input.vault ?? null, isWritable: true },
resolverEntry: { value: input.resolverEntry ?? null, isWritable: false },
systemProgram: { value: input.systemProgram ?? null, isWritable: false }, systemProgram: { value: input.systemProgram ?? null, isWritable: false },
}; };
const accounts = originalAccounts as Record< const accounts = originalAccounts as Record<
@@ -315,8 +351,10 @@ export function getCreateEscrowInstruction<
accounts: [ accounts: [
getAccountMeta("seller", accounts.seller), getAccountMeta("seller", accounts.seller),
getAccountMeta("buyer", accounts.buyer), getAccountMeta("buyer", accounts.buyer),
getAccountMeta("resolver", accounts.resolver),
getAccountMeta("escrowAccount", accounts.escrowAccount), getAccountMeta("escrowAccount", accounts.escrowAccount),
getAccountMeta("vault", accounts.vault), getAccountMeta("vault", accounts.vault),
getAccountMeta("resolverEntry", accounts.resolverEntry),
getAccountMeta("systemProgram", accounts.systemProgram), getAccountMeta("systemProgram", accounts.systemProgram),
], ],
data: getCreateEscrowInstructionDataEncoder().encode( data: getCreateEscrowInstructionDataEncoder().encode(
@@ -327,8 +365,10 @@ export function getCreateEscrowInstruction<
TProgramAddress, TProgramAddress,
TAccountSeller, TAccountSeller,
TAccountBuyer, TAccountBuyer,
TAccountResolver,
TAccountEscrowAccount, TAccountEscrowAccount,
TAccountVault, TAccountVault,
TAccountResolverEntry,
TAccountSystemProgram TAccountSystemProgram
>); >);
} }
@@ -341,9 +381,11 @@ export type ParsedCreateEscrowInstruction<
accounts: { accounts: {
seller: TAccountMetas[0]; seller: TAccountMetas[0];
buyer: TAccountMetas[1]; buyer: TAccountMetas[1];
escrowAccount: TAccountMetas[2]; resolver: TAccountMetas[2];
vault: TAccountMetas[3]; escrowAccount: TAccountMetas[3];
systemProgram: TAccountMetas[4]; vault: TAccountMetas[4];
resolverEntry: TAccountMetas[5];
systemProgram: TAccountMetas[6];
}; };
data: CreateEscrowInstructionData; data: CreateEscrowInstructionData;
}; };
@@ -356,12 +398,12 @@ export function parseCreateEscrowInstruction<
InstructionWithAccounts<TAccountMetas> & InstructionWithAccounts<TAccountMetas> &
InstructionWithData<ReadonlyUint8Array>, InstructionWithData<ReadonlyUint8Array>,
): ParsedCreateEscrowInstruction<TProgram, TAccountMetas> { ): ParsedCreateEscrowInstruction<TProgram, TAccountMetas> {
if (instruction.accounts.length < 5) { if (instruction.accounts.length < 7) {
throw new SolanaError( throw new SolanaError(
SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS, SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS,
{ {
actualAccountMetas: instruction.accounts.length, actualAccountMetas: instruction.accounts.length,
expectedAccountMetas: 5, expectedAccountMetas: 7,
}, },
); );
} }
@@ -376,8 +418,10 @@ export function parseCreateEscrowInstruction<
accounts: { accounts: {
seller: getNextAccount(), seller: getNextAccount(),
buyer: getNextAccount(), buyer: getNextAccount(),
resolver: getNextAccount(),
escrowAccount: getNextAccount(), escrowAccount: getNextAccount(),
vault: getNextAccount(), vault: getNextAccount(),
resolverEntry: getNextAccount(),
systemProgram: getNextAccount(), systemProgram: getNextAccount(),
}, },
data: getCreateEscrowInstructionDataDecoder().decode(instruction.data), data: getCreateEscrowInstructionDataDecoder().decode(instruction.data),

View File

@@ -0,0 +1,382 @@
/**
* This code was AUTOGENERATED using the Codama library.
* Please DO NOT EDIT THIS FILE, instead use visitors
* to add features, then rerun Codama to update it.
*
* @see https://github.com/codama-idl/codama
*/
import {
combineCodec,
fixDecoderSize,
fixEncoderSize,
getBytesDecoder,
getBytesEncoder,
getStructDecoder,
getStructEncoder,
SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS,
SolanaError,
transformEncoder,
type AccountMeta,
type AccountSignerMeta,
type Address,
type FixedSizeCodec,
type FixedSizeDecoder,
type FixedSizeEncoder,
type Instruction,
type InstructionWithAccounts,
type InstructionWithData,
type ReadonlyAccount,
type ReadonlySignerAccount,
type ReadonlyUint8Array,
type TransactionSigner,
type WritableAccount,
type WritableSignerAccount,
} from "@solana/kit";
import {
getAccountMetaFactory,
getAddressFromResolvedInstructionAccount,
type ResolvedInstructionAccount,
} from "@solana/program-client-core";
import { findVaultPda } from "../pdas";
import { DESCRO_PROGRAM_ADDRESS } from "../programs";
import {
getWinnerDecoder,
getWinnerEncoder,
type Winner,
type WinnerArgs,
} from "../types";
export const EMERGENCY_RESOLVE_DISCRIMINATOR: ReadonlyUint8Array =
new Uint8Array([63, 112, 185, 42, 47, 61, 232, 79]);
export function getEmergencyResolveDiscriminatorBytes(): ReadonlyUint8Array {
return fixEncoderSize(getBytesEncoder(), 8).encode(
EMERGENCY_RESOLVE_DISCRIMINATOR,
);
}
export type EmergencyResolveInstruction<
TProgram extends string = typeof DESCRO_PROGRAM_ADDRESS,
TAccountBuyer extends string | AccountMeta<string> = string,
TAccountSeller extends string | AccountMeta<string> = string,
TAccountWinner extends string | AccountMeta<string> = string,
TAccountEscrowAccount extends string | AccountMeta<string> = string,
TAccountVault extends string | AccountMeta<string> = string,
TAccountSystemProgram extends string | AccountMeta<string> =
"11111111111111111111111111111111",
TRemainingAccounts extends readonly AccountMeta<string>[] = [],
> = Instruction<TProgram> &
InstructionWithData<ReadonlyUint8Array> &
InstructionWithAccounts<
[
TAccountBuyer extends string
? ReadonlySignerAccount<TAccountBuyer> &
AccountSignerMeta<TAccountBuyer>
: TAccountBuyer,
TAccountSeller extends string
? WritableSignerAccount<TAccountSeller> &
AccountSignerMeta<TAccountSeller>
: TAccountSeller,
TAccountWinner extends string
? WritableAccount<TAccountWinner>
: TAccountWinner,
TAccountEscrowAccount extends string
? WritableAccount<TAccountEscrowAccount>
: TAccountEscrowAccount,
TAccountVault extends string
? WritableAccount<TAccountVault>
: TAccountVault,
TAccountSystemProgram extends string
? ReadonlyAccount<TAccountSystemProgram>
: TAccountSystemProgram,
...TRemainingAccounts,
]
>;
export type EmergencyResolveInstructionData = {
discriminator: ReadonlyUint8Array;
winner: Winner;
};
export type EmergencyResolveInstructionDataArgs = { winner: WinnerArgs };
export function getEmergencyResolveInstructionDataEncoder(): FixedSizeEncoder<EmergencyResolveInstructionDataArgs> {
return transformEncoder(
getStructEncoder([
["discriminator", fixEncoderSize(getBytesEncoder(), 8)],
["winner", getWinnerEncoder()],
]),
(value) => ({ ...value, discriminator: EMERGENCY_RESOLVE_DISCRIMINATOR }),
);
}
export function getEmergencyResolveInstructionDataDecoder(): FixedSizeDecoder<EmergencyResolveInstructionData> {
return getStructDecoder([
["discriminator", fixDecoderSize(getBytesDecoder(), 8)],
["winner", getWinnerDecoder()],
]);
}
export function getEmergencyResolveInstructionDataCodec(): FixedSizeCodec<
EmergencyResolveInstructionDataArgs,
EmergencyResolveInstructionData
> {
return combineCodec(
getEmergencyResolveInstructionDataEncoder(),
getEmergencyResolveInstructionDataDecoder(),
);
}
export type EmergencyResolveAsyncInput<
TAccountBuyer extends string = string,
TAccountSeller extends string = string,
TAccountWinner extends string = string,
TAccountEscrowAccount extends string = string,
TAccountVault extends string = string,
TAccountSystemProgram extends string = string,
> = {
buyer: TransactionSigner<TAccountBuyer>;
seller: TransactionSigner<TAccountSeller>;
winner: Address<TAccountWinner>;
escrowAccount: Address<TAccountEscrowAccount>;
vault?: Address<TAccountVault>;
systemProgram?: Address<TAccountSystemProgram>;
winnerArg: EmergencyResolveInstructionDataArgs["winner"];
};
export async function getEmergencyResolveInstructionAsync<
TAccountBuyer extends string,
TAccountSeller extends string,
TAccountWinner extends string,
TAccountEscrowAccount extends string,
TAccountVault extends string,
TAccountSystemProgram extends string,
TProgramAddress extends Address = typeof DESCRO_PROGRAM_ADDRESS,
>(
input: EmergencyResolveAsyncInput<
TAccountBuyer,
TAccountSeller,
TAccountWinner,
TAccountEscrowAccount,
TAccountVault,
TAccountSystemProgram
>,
config?: { programAddress?: TProgramAddress },
): Promise<
EmergencyResolveInstruction<
TProgramAddress,
TAccountBuyer,
TAccountSeller,
TAccountWinner,
TAccountEscrowAccount,
TAccountVault,
TAccountSystemProgram
>
> {
// Program address.
const programAddress = config?.programAddress ?? DESCRO_PROGRAM_ADDRESS;
// Original accounts.
const originalAccounts = {
buyer: { value: input.buyer ?? null, isWritable: false },
seller: { value: input.seller ?? null, isWritable: true },
winner: { value: input.winner ?? null, isWritable: true },
escrowAccount: { value: input.escrowAccount ?? null, isWritable: true },
vault: { value: input.vault ?? null, isWritable: true },
systemProgram: { value: input.systemProgram ?? null, isWritable: false },
};
const accounts = originalAccounts as Record<
keyof typeof originalAccounts,
ResolvedInstructionAccount
>;
// Original args.
const args = { ...input, winner: input.winnerArg };
// Resolve default values.
if (!accounts.vault.value) {
accounts.vault.value = await findVaultPda({
escrowAccount: getAddressFromResolvedInstructionAccount(
"escrowAccount",
accounts.escrowAccount.value,
),
});
}
if (!accounts.systemProgram.value) {
accounts.systemProgram.value =
"11111111111111111111111111111111" as Address<"11111111111111111111111111111111">;
}
const getAccountMeta = getAccountMetaFactory(programAddress, "programId");
return Object.freeze({
accounts: [
getAccountMeta("buyer", accounts.buyer),
getAccountMeta("seller", accounts.seller),
getAccountMeta("winner", accounts.winner),
getAccountMeta("escrowAccount", accounts.escrowAccount),
getAccountMeta("vault", accounts.vault),
getAccountMeta("systemProgram", accounts.systemProgram),
],
data: getEmergencyResolveInstructionDataEncoder().encode(
args as EmergencyResolveInstructionDataArgs,
),
programAddress,
} as EmergencyResolveInstruction<
TProgramAddress,
TAccountBuyer,
TAccountSeller,
TAccountWinner,
TAccountEscrowAccount,
TAccountVault,
TAccountSystemProgram
>);
}
export type EmergencyResolveInput<
TAccountBuyer extends string = string,
TAccountSeller extends string = string,
TAccountWinner extends string = string,
TAccountEscrowAccount extends string = string,
TAccountVault extends string = string,
TAccountSystemProgram extends string = string,
> = {
buyer: TransactionSigner<TAccountBuyer>;
seller: TransactionSigner<TAccountSeller>;
winner: Address<TAccountWinner>;
escrowAccount: Address<TAccountEscrowAccount>;
vault: Address<TAccountVault>;
systemProgram?: Address<TAccountSystemProgram>;
winnerArg: EmergencyResolveInstructionDataArgs["winner"];
};
export function getEmergencyResolveInstruction<
TAccountBuyer extends string,
TAccountSeller extends string,
TAccountWinner extends string,
TAccountEscrowAccount extends string,
TAccountVault extends string,
TAccountSystemProgram extends string,
TProgramAddress extends Address = typeof DESCRO_PROGRAM_ADDRESS,
>(
input: EmergencyResolveInput<
TAccountBuyer,
TAccountSeller,
TAccountWinner,
TAccountEscrowAccount,
TAccountVault,
TAccountSystemProgram
>,
config?: { programAddress?: TProgramAddress },
): EmergencyResolveInstruction<
TProgramAddress,
TAccountBuyer,
TAccountSeller,
TAccountWinner,
TAccountEscrowAccount,
TAccountVault,
TAccountSystemProgram
> {
// Program address.
const programAddress = config?.programAddress ?? DESCRO_PROGRAM_ADDRESS;
// Original accounts.
const originalAccounts = {
buyer: { value: input.buyer ?? null, isWritable: false },
seller: { value: input.seller ?? null, isWritable: true },
winner: { value: input.winner ?? null, isWritable: true },
escrowAccount: { value: input.escrowAccount ?? null, isWritable: true },
vault: { value: input.vault ?? null, isWritable: true },
systemProgram: { value: input.systemProgram ?? null, isWritable: false },
};
const accounts = originalAccounts as Record<
keyof typeof originalAccounts,
ResolvedInstructionAccount
>;
// Original args.
const args = { ...input, winner: input.winnerArg };
// Resolve default values.
if (!accounts.systemProgram.value) {
accounts.systemProgram.value =
"11111111111111111111111111111111" as Address<"11111111111111111111111111111111">;
}
const getAccountMeta = getAccountMetaFactory(programAddress, "programId");
return Object.freeze({
accounts: [
getAccountMeta("buyer", accounts.buyer),
getAccountMeta("seller", accounts.seller),
getAccountMeta("winner", accounts.winner),
getAccountMeta("escrowAccount", accounts.escrowAccount),
getAccountMeta("vault", accounts.vault),
getAccountMeta("systemProgram", accounts.systemProgram),
],
data: getEmergencyResolveInstructionDataEncoder().encode(
args as EmergencyResolveInstructionDataArgs,
),
programAddress,
} as EmergencyResolveInstruction<
TProgramAddress,
TAccountBuyer,
TAccountSeller,
TAccountWinner,
TAccountEscrowAccount,
TAccountVault,
TAccountSystemProgram
>);
}
export type ParsedEmergencyResolveInstruction<
TProgram extends string = typeof DESCRO_PROGRAM_ADDRESS,
TAccountMetas extends readonly AccountMeta[] = readonly AccountMeta[],
> = {
programAddress: Address<TProgram>;
accounts: {
buyer: TAccountMetas[0];
seller: TAccountMetas[1];
winner: TAccountMetas[2];
escrowAccount: TAccountMetas[3];
vault: TAccountMetas[4];
systemProgram: TAccountMetas[5];
};
data: EmergencyResolveInstructionData;
};
export function parseEmergencyResolveInstruction<
TProgram extends string,
TAccountMetas extends readonly AccountMeta[],
>(
instruction: Instruction<TProgram> &
InstructionWithAccounts<TAccountMetas> &
InstructionWithData<ReadonlyUint8Array>,
): ParsedEmergencyResolveInstruction<TProgram, TAccountMetas> {
if (instruction.accounts.length < 6) {
throw new SolanaError(
SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS,
{
actualAccountMetas: instruction.accounts.length,
expectedAccountMetas: 6,
},
);
}
let accountIndex = 0;
const getNextAccount = () => {
const accountMeta = (instruction.accounts as TAccountMetas)[accountIndex]!;
accountIndex += 1;
return accountMeta;
};
return {
programAddress: instruction.programAddress,
accounts: {
buyer: getNextAccount(),
seller: getNextAccount(),
winner: getNextAccount(),
escrowAccount: getNextAccount(),
vault: getNextAccount(),
systemProgram: getNextAccount(),
},
data: getEmergencyResolveInstructionDataDecoder().decode(instruction.data),
};
}

View File

@@ -6,9 +6,12 @@
* @see https://github.com/codama-idl/codama * @see https://github.com/codama-idl/codama
*/ */
export * from "./buyerCreateEscrow";
export * from "./cancel"; export * from "./cancel";
export * from "./complete"; export * from "./complete";
export * from "./createEscrow"; export * from "./createEscrow";
export * from "./deposit"; export * from "./deposit";
export * from "./dispute"; export * from "./dispute";
export * from "./emergencyResolve";
export * from "./resolve"; export * from "./resolve";
export * from "./sellerConfirm";

View File

@@ -0,0 +1,222 @@
/**
* This code was AUTOGENERATED using the Codama library.
* Please DO NOT EDIT THIS FILE, instead use visitors
* to add features, then rerun Codama to update it.
*
* @see https://github.com/codama-idl/codama
*/
import {
combineCodec,
fixDecoderSize,
fixEncoderSize,
getBytesDecoder,
getBytesEncoder,
getStructDecoder,
getStructEncoder,
SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS,
SolanaError,
transformEncoder,
type AccountMeta,
type AccountSignerMeta,
type Address,
type FixedSizeCodec,
type FixedSizeDecoder,
type FixedSizeEncoder,
type Instruction,
type InstructionWithAccounts,
type InstructionWithData,
type ReadonlyAccount,
type ReadonlySignerAccount,
type ReadonlyUint8Array,
type TransactionSigner,
type WritableAccount,
} from "@solana/kit";
import {
getAccountMetaFactory,
type ResolvedInstructionAccount,
} from "@solana/program-client-core";
import { DESCRO_PROGRAM_ADDRESS } from "../programs";
export const SELLER_CONFIRM_DISCRIMINATOR: ReadonlyUint8Array = new Uint8Array([
21, 244, 3, 109, 138, 95, 198, 242,
]);
export function getSellerConfirmDiscriminatorBytes(): ReadonlyUint8Array {
return fixEncoderSize(getBytesEncoder(), 8).encode(
SELLER_CONFIRM_DISCRIMINATOR,
);
}
export type SellerConfirmInstruction<
TProgram extends string = typeof DESCRO_PROGRAM_ADDRESS,
TAccountSeller extends string | AccountMeta<string> = string,
TAccountResolver extends string | AccountMeta<string> = string,
TAccountEscrowAccount extends string | AccountMeta<string> = string,
TAccountResolverEntry extends string | AccountMeta<string> = string,
TRemainingAccounts extends readonly AccountMeta<string>[] = [],
> = Instruction<TProgram> &
InstructionWithData<ReadonlyUint8Array> &
InstructionWithAccounts<
[
TAccountSeller extends string
? ReadonlySignerAccount<TAccountSeller> &
AccountSignerMeta<TAccountSeller>
: TAccountSeller,
TAccountResolver extends string
? ReadonlyAccount<TAccountResolver>
: TAccountResolver,
TAccountEscrowAccount extends string
? WritableAccount<TAccountEscrowAccount>
: TAccountEscrowAccount,
TAccountResolverEntry extends string
? ReadonlyAccount<TAccountResolverEntry>
: TAccountResolverEntry,
...TRemainingAccounts,
]
>;
export type SellerConfirmInstructionData = {
discriminator: ReadonlyUint8Array;
};
export type SellerConfirmInstructionDataArgs = {};
export function getSellerConfirmInstructionDataEncoder(): FixedSizeEncoder<SellerConfirmInstructionDataArgs> {
return transformEncoder(
getStructEncoder([["discriminator", fixEncoderSize(getBytesEncoder(), 8)]]),
(value) => ({ ...value, discriminator: SELLER_CONFIRM_DISCRIMINATOR }),
);
}
export function getSellerConfirmInstructionDataDecoder(): FixedSizeDecoder<SellerConfirmInstructionData> {
return getStructDecoder([
["discriminator", fixDecoderSize(getBytesDecoder(), 8)],
]);
}
export function getSellerConfirmInstructionDataCodec(): FixedSizeCodec<
SellerConfirmInstructionDataArgs,
SellerConfirmInstructionData
> {
return combineCodec(
getSellerConfirmInstructionDataEncoder(),
getSellerConfirmInstructionDataDecoder(),
);
}
export type SellerConfirmInput<
TAccountSeller extends string = string,
TAccountResolver extends string = string,
TAccountEscrowAccount extends string = string,
TAccountResolverEntry extends string = string,
> = {
seller: TransactionSigner<TAccountSeller>;
resolver: Address<TAccountResolver>;
escrowAccount: Address<TAccountEscrowAccount>;
resolverEntry: Address<TAccountResolverEntry>;
};
export function getSellerConfirmInstruction<
TAccountSeller extends string,
TAccountResolver extends string,
TAccountEscrowAccount extends string,
TAccountResolverEntry extends string,
TProgramAddress extends Address = typeof DESCRO_PROGRAM_ADDRESS,
>(
input: SellerConfirmInput<
TAccountSeller,
TAccountResolver,
TAccountEscrowAccount,
TAccountResolverEntry
>,
config?: { programAddress?: TProgramAddress },
): SellerConfirmInstruction<
TProgramAddress,
TAccountSeller,
TAccountResolver,
TAccountEscrowAccount,
TAccountResolverEntry
> {
// Program address.
const programAddress = config?.programAddress ?? DESCRO_PROGRAM_ADDRESS;
// Original accounts.
const originalAccounts = {
seller: { value: input.seller ?? null, isWritable: false },
resolver: { value: input.resolver ?? null, isWritable: false },
escrowAccount: { value: input.escrowAccount ?? null, isWritable: true },
resolverEntry: { value: input.resolverEntry ?? null, isWritable: false },
};
const accounts = originalAccounts as Record<
keyof typeof originalAccounts,
ResolvedInstructionAccount
>;
const getAccountMeta = getAccountMetaFactory(programAddress, "programId");
return Object.freeze({
accounts: [
getAccountMeta("seller", accounts.seller),
getAccountMeta("resolver", accounts.resolver),
getAccountMeta("escrowAccount", accounts.escrowAccount),
getAccountMeta("resolverEntry", accounts.resolverEntry),
],
data: getSellerConfirmInstructionDataEncoder().encode({}),
programAddress,
} as SellerConfirmInstruction<
TProgramAddress,
TAccountSeller,
TAccountResolver,
TAccountEscrowAccount,
TAccountResolverEntry
>);
}
export type ParsedSellerConfirmInstruction<
TProgram extends string = typeof DESCRO_PROGRAM_ADDRESS,
TAccountMetas extends readonly AccountMeta[] = readonly AccountMeta[],
> = {
programAddress: Address<TProgram>;
accounts: {
seller: TAccountMetas[0];
resolver: TAccountMetas[1];
escrowAccount: TAccountMetas[2];
resolverEntry: TAccountMetas[3];
};
data: SellerConfirmInstructionData;
};
export function parseSellerConfirmInstruction<
TProgram extends string,
TAccountMetas extends readonly AccountMeta[],
>(
instruction: Instruction<TProgram> &
InstructionWithAccounts<TAccountMetas> &
InstructionWithData<ReadonlyUint8Array>,
): ParsedSellerConfirmInstruction<TProgram, TAccountMetas> {
if (instruction.accounts.length < 4) {
throw new SolanaError(
SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS,
{
actualAccountMetas: instruction.accounts.length,
expectedAccountMetas: 4,
},
);
}
let accountIndex = 0;
const getNextAccount = () => {
const accountMeta = (instruction.accounts as TAccountMetas)[accountIndex]!;
accountIndex += 1;
return accountMeta;
};
return {
programAddress: instruction.programAddress,
accounts: {
seller: getNextAccount(),
resolver: getNextAccount(),
escrowAccount: getNextAccount(),
resolverEntry: getNextAccount(),
},
data: getSellerConfirmInstructionDataDecoder().decode(instruction.data),
};
}

View File

@@ -38,30 +38,42 @@ import {
type EscrowAccountArgs, type EscrowAccountArgs,
} from "../accounts"; } from "../accounts";
import { import {
getCancelInstruction, getBuyerCreateEscrowInstructionAsync,
getCancelInstructionAsync,
getCompleteInstructionAsync, getCompleteInstructionAsync,
getCreateEscrowInstructionAsync, getCreateEscrowInstructionAsync,
getDepositInstructionAsync, getDepositInstructionAsync,
getDisputeInstruction, getDisputeInstruction,
getEmergencyResolveInstructionAsync,
getResolveInstructionAsync, getResolveInstructionAsync,
getSellerConfirmInstruction,
parseBuyerCreateEscrowInstruction,
parseCancelInstruction, parseCancelInstruction,
parseCompleteInstruction, parseCompleteInstruction,
parseCreateEscrowInstruction, parseCreateEscrowInstruction,
parseDepositInstruction, parseDepositInstruction,
parseDisputeInstruction, parseDisputeInstruction,
parseEmergencyResolveInstruction,
parseResolveInstruction, parseResolveInstruction,
type CancelInput, parseSellerConfirmInstruction,
type BuyerCreateEscrowAsyncInput,
type CancelAsyncInput,
type CompleteAsyncInput, type CompleteAsyncInput,
type CreateEscrowAsyncInput, type CreateEscrowAsyncInput,
type DepositAsyncInput, type DepositAsyncInput,
type DisputeInput, type DisputeInput,
type EmergencyResolveAsyncInput,
type ParsedBuyerCreateEscrowInstruction,
type ParsedCancelInstruction, type ParsedCancelInstruction,
type ParsedCompleteInstruction, type ParsedCompleteInstruction,
type ParsedCreateEscrowInstruction, type ParsedCreateEscrowInstruction,
type ParsedDepositInstruction, type ParsedDepositInstruction,
type ParsedDisputeInstruction, type ParsedDisputeInstruction,
type ParsedEmergencyResolveInstruction,
type ParsedResolveInstruction, type ParsedResolveInstruction,
type ParsedSellerConfirmInstruction,
type ResolveAsyncInput, type ResolveAsyncInput,
type SellerConfirmInput,
} from "../instructions"; } from "../instructions";
import { import {
findEscrowAccountPda, findEscrowAccountPda,
@@ -98,18 +110,32 @@ export function identifyDescroAccount(
} }
export enum DescroInstruction { export enum DescroInstruction {
BuyerCreateEscrow,
Cancel, Cancel,
Complete, Complete,
CreateEscrow, CreateEscrow,
Deposit, Deposit,
Dispute, Dispute,
EmergencyResolve,
Resolve, Resolve,
SellerConfirm,
} }
export function identifyDescroInstruction( export function identifyDescroInstruction(
instruction: { data: ReadonlyUint8Array } | ReadonlyUint8Array, instruction: { data: ReadonlyUint8Array } | ReadonlyUint8Array,
): DescroInstruction { ): DescroInstruction {
const data = "data" in instruction ? instruction.data : instruction; const data = "data" in instruction ? instruction.data : instruction;
if (
containsBytes(
data,
fixEncoderSize(getBytesEncoder(), 8).encode(
new Uint8Array([79, 199, 251, 30, 163, 86, 13, 73]),
),
0,
)
) {
return DescroInstruction.BuyerCreateEscrow;
}
if ( if (
containsBytes( containsBytes(
data, data,
@@ -165,6 +191,17 @@ export function identifyDescroInstruction(
) { ) {
return DescroInstruction.Dispute; return DescroInstruction.Dispute;
} }
if (
containsBytes(
data,
fixEncoderSize(getBytesEncoder(), 8).encode(
new Uint8Array([63, 112, 185, 42, 47, 61, 232, 79]),
),
0,
)
) {
return DescroInstruction.EmergencyResolve;
}
if ( if (
containsBytes( containsBytes(
data, data,
@@ -176,6 +213,17 @@ export function identifyDescroInstruction(
) { ) {
return DescroInstruction.Resolve; return DescroInstruction.Resolve;
} }
if (
containsBytes(
data,
fixEncoderSize(getBytesEncoder(), 8).encode(
new Uint8Array([21, 244, 3, 109, 138, 95, 198, 242]),
),
0,
)
) {
return DescroInstruction.SellerConfirm;
}
throw new SolanaError( throw new SolanaError(
SOLANA_ERROR__PROGRAM_CLIENTS__FAILED_TO_IDENTIFY_INSTRUCTION, SOLANA_ERROR__PROGRAM_CLIENTS__FAILED_TO_IDENTIFY_INSTRUCTION,
{ instructionData: data, programName: "descro" }, { instructionData: data, programName: "descro" },
@@ -185,6 +233,9 @@ export function identifyDescroInstruction(
export type ParsedDescroInstruction< export type ParsedDescroInstruction<
TProgram extends string = "DjVR4EuYV6USMJFfsGZwhZ3y8rtWsmG8EvDY96GTqqi3", TProgram extends string = "DjVR4EuYV6USMJFfsGZwhZ3y8rtWsmG8EvDY96GTqqi3",
> = > =
| ({
instructionType: DescroInstruction.BuyerCreateEscrow;
} & ParsedBuyerCreateEscrowInstruction<TProgram>)
| ({ | ({
instructionType: DescroInstruction.Cancel; instructionType: DescroInstruction.Cancel;
} & ParsedCancelInstruction<TProgram>) } & ParsedCancelInstruction<TProgram>)
@@ -200,15 +251,28 @@ export type ParsedDescroInstruction<
| ({ | ({
instructionType: DescroInstruction.Dispute; instructionType: DescroInstruction.Dispute;
} & ParsedDisputeInstruction<TProgram>) } & ParsedDisputeInstruction<TProgram>)
| ({
instructionType: DescroInstruction.EmergencyResolve;
} & ParsedEmergencyResolveInstruction<TProgram>)
| ({ | ({
instructionType: DescroInstruction.Resolve; instructionType: DescroInstruction.Resolve;
} & ParsedResolveInstruction<TProgram>); } & ParsedResolveInstruction<TProgram>)
| ({
instructionType: DescroInstruction.SellerConfirm;
} & ParsedSellerConfirmInstruction<TProgram>);
export function parseDescroInstruction<TProgram extends string>( export function parseDescroInstruction<TProgram extends string>(
instruction: Instruction<TProgram> & InstructionWithData<ReadonlyUint8Array>, instruction: Instruction<TProgram> & InstructionWithData<ReadonlyUint8Array>,
): ParsedDescroInstruction<TProgram> { ): ParsedDescroInstruction<TProgram> {
const instructionType = identifyDescroInstruction(instruction); const instructionType = identifyDescroInstruction(instruction);
switch (instructionType) { switch (instructionType) {
case DescroInstruction.BuyerCreateEscrow: {
assertIsInstructionWithAccounts(instruction);
return {
instructionType: DescroInstruction.BuyerCreateEscrow,
...parseBuyerCreateEscrowInstruction(instruction),
};
}
case DescroInstruction.Cancel: { case DescroInstruction.Cancel: {
assertIsInstructionWithAccounts(instruction); assertIsInstructionWithAccounts(instruction);
return { return {
@@ -244,6 +308,13 @@ export function parseDescroInstruction<TProgram extends string>(
...parseDisputeInstruction(instruction), ...parseDisputeInstruction(instruction),
}; };
} }
case DescroInstruction.EmergencyResolve: {
assertIsInstructionWithAccounts(instruction);
return {
instructionType: DescroInstruction.EmergencyResolve,
...parseEmergencyResolveInstruction(instruction),
};
}
case DescroInstruction.Resolve: { case DescroInstruction.Resolve: {
assertIsInstructionWithAccounts(instruction); assertIsInstructionWithAccounts(instruction);
return { return {
@@ -251,6 +322,13 @@ export function parseDescroInstruction<TProgram extends string>(
...parseResolveInstruction(instruction), ...parseResolveInstruction(instruction),
}; };
} }
case DescroInstruction.SellerConfirm: {
assertIsInstructionWithAccounts(instruction);
return {
instructionType: DescroInstruction.SellerConfirm,
...parseSellerConfirmInstruction(instruction),
};
}
default: default:
throw new SolanaError( throw new SolanaError(
SOLANA_ERROR__PROGRAM_CLIENTS__UNRECOGNIZED_INSTRUCTION_TYPE, SOLANA_ERROR__PROGRAM_CLIENTS__UNRECOGNIZED_INSTRUCTION_TYPE,
@@ -271,9 +349,13 @@ export type DescroPluginAccounts = {
}; };
export type DescroPluginInstructions = { export type DescroPluginInstructions = {
buyerCreateEscrow: (
input: BuyerCreateEscrowAsyncInput,
) => ReturnType<typeof getBuyerCreateEscrowInstructionAsync> &
SelfPlanAndSendFunctions;
cancel: ( cancel: (
input: CancelInput, input: CancelAsyncInput,
) => ReturnType<typeof getCancelInstruction> & SelfPlanAndSendFunctions; ) => ReturnType<typeof getCancelInstructionAsync> & SelfPlanAndSendFunctions;
complete: ( complete: (
input: CompleteAsyncInput, input: CompleteAsyncInput,
) => ReturnType<typeof getCompleteInstructionAsync> & ) => ReturnType<typeof getCompleteInstructionAsync> &
@@ -288,14 +370,22 @@ export type DescroPluginInstructions = {
dispute: ( dispute: (
input: DisputeInput, input: DisputeInput,
) => ReturnType<typeof getDisputeInstruction> & SelfPlanAndSendFunctions; ) => ReturnType<typeof getDisputeInstruction> & SelfPlanAndSendFunctions;
emergencyResolve: (
input: EmergencyResolveAsyncInput,
) => ReturnType<typeof getEmergencyResolveInstructionAsync> &
SelfPlanAndSendFunctions;
resolve: ( resolve: (
input: ResolveAsyncInput, input: ResolveAsyncInput,
) => ReturnType<typeof getResolveInstructionAsync> & SelfPlanAndSendFunctions; ) => ReturnType<typeof getResolveInstructionAsync> & SelfPlanAndSendFunctions;
sellerConfirm: (
input: SellerConfirmInput,
) => ReturnType<typeof getSellerConfirmInstruction> &
SelfPlanAndSendFunctions;
}; };
export type DescroPluginPdas = { export type DescroPluginPdas = {
vault: typeof findVaultPda;
escrowAccount: typeof findEscrowAccountPda; escrowAccount: typeof findEscrowAccountPda;
vault: typeof findVaultPda;
escrowAuthority: typeof findEscrowAuthorityPda; escrowAuthority: typeof findEscrowAuthorityPda;
}; };
@@ -315,8 +405,16 @@ export function descroProgram() {
escrowAccount: addSelfFetchFunctions(client, getEscrowAccountCodec()), escrowAccount: addSelfFetchFunctions(client, getEscrowAccountCodec()),
}, },
instructions: { instructions: {
buyerCreateEscrow: (input) =>
addSelfPlanAndSendFunctions(
client,
getBuyerCreateEscrowInstructionAsync(input),
),
cancel: (input) => cancel: (input) =>
addSelfPlanAndSendFunctions(client, getCancelInstruction(input)), addSelfPlanAndSendFunctions(
client,
getCancelInstructionAsync(input),
),
complete: (input) => complete: (input) =>
addSelfPlanAndSendFunctions( addSelfPlanAndSendFunctions(
client, client,
@@ -334,15 +432,25 @@ export function descroProgram() {
), ),
dispute: (input) => dispute: (input) =>
addSelfPlanAndSendFunctions(client, getDisputeInstruction(input)), addSelfPlanAndSendFunctions(client, getDisputeInstruction(input)),
emergencyResolve: (input) =>
addSelfPlanAndSendFunctions(
client,
getEmergencyResolveInstructionAsync(input),
),
resolve: (input) => resolve: (input) =>
addSelfPlanAndSendFunctions( addSelfPlanAndSendFunctions(
client, client,
getResolveInstructionAsync(input), getResolveInstructionAsync(input),
), ),
sellerConfirm: (input) =>
addSelfPlanAndSendFunctions(
client,
getSellerConfirmInstruction(input),
),
}, },
pdas: { pdas: {
vault: findVaultPda,
escrowAccount: findEscrowAccountPda, escrowAccount: findEscrowAccountPda,
vault: findVaultPda,
escrowAuthority: findEscrowAuthorityPda, escrowAuthority: findEscrowAuthorityPda,
}, },
}, },

View File

@@ -17,6 +17,7 @@ import {
export enum EscrowState { export enum EscrowState {
AwaitingDeposit, AwaitingDeposit,
AwaitingSellerConfirm,
Active, Active,
Disputed, Disputed,
Complete, Complete,

View File

@@ -47,8 +47,12 @@ import {
type ReadonlyUint8Array, type ReadonlyUint8Array,
} from "@solana/kit"; } from "@solana/kit";
import { import {
getAcceptancePolicyDecoder,
getAcceptancePolicyEncoder,
getResolverTypeDecoder, getResolverTypeDecoder,
getResolverTypeEncoder, getResolverTypeEncoder,
type AcceptancePolicy,
type AcceptancePolicyArgs,
type ResolverType, type ResolverType,
type ResolverTypeArgs, type ResolverTypeArgs,
} from "../types"; } from "../types";
@@ -67,6 +71,7 @@ export type ResolverEntry = {
discriminator: ReadonlyUint8Array; discriminator: ReadonlyUint8Array;
authority: Address; authority: Address;
resolverType: ResolverType; resolverType: ResolverType;
acceptancePolicy: AcceptancePolicy;
name: string; name: string;
description: string; description: string;
feeBps: number; feeBps: number;
@@ -81,6 +86,7 @@ export type ResolverEntry = {
export type ResolverEntryArgs = { export type ResolverEntryArgs = {
authority: Address; authority: Address;
resolverType: ResolverTypeArgs; resolverType: ResolverTypeArgs;
acceptancePolicy: AcceptancePolicyArgs;
name: string; name: string;
description: string; description: string;
feeBps: number; feeBps: number;
@@ -99,6 +105,7 @@ export function getResolverEntryEncoder(): Encoder<ResolverEntryArgs> {
["discriminator", fixEncoderSize(getBytesEncoder(), 8)], ["discriminator", fixEncoderSize(getBytesEncoder(), 8)],
["authority", getAddressEncoder()], ["authority", getAddressEncoder()],
["resolverType", getResolverTypeEncoder()], ["resolverType", getResolverTypeEncoder()],
["acceptancePolicy", getAcceptancePolicyEncoder()],
["name", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())], ["name", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
["description", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())], ["description", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
["feeBps", getU16Encoder()], ["feeBps", getU16Encoder()],
@@ -119,6 +126,7 @@ export function getResolverEntryDecoder(): Decoder<ResolverEntry> {
["discriminator", fixDecoderSize(getBytesDecoder(), 8)], ["discriminator", fixDecoderSize(getBytesDecoder(), 8)],
["authority", getAddressDecoder()], ["authority", getAddressDecoder()],
["resolverType", getResolverTypeDecoder()], ["resolverType", getResolverTypeDecoder()],
["acceptancePolicy", getAcceptancePolicyDecoder()],
["name", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())], ["name", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
["description", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())], ["description", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
["feeBps", getU16Decoder()], ["feeBps", getU16Decoder()],

View File

@@ -50,8 +50,12 @@ import {
import { findResolverEntryPda } from "../pdas"; import { findResolverEntryPda } from "../pdas";
import { DESCRO_EXT_RESOLVERS_PROGRAM_ADDRESS } from "../programs"; import { DESCRO_EXT_RESOLVERS_PROGRAM_ADDRESS } from "../programs";
import { import {
getAcceptancePolicyDecoder,
getAcceptancePolicyEncoder,
getResolverTypeDecoder, getResolverTypeDecoder,
getResolverTypeEncoder, getResolverTypeEncoder,
type AcceptancePolicy,
type AcceptancePolicyArgs,
type ResolverType, type ResolverType,
type ResolverTypeArgs, type ResolverTypeArgs,
} from "../types"; } from "../types";
@@ -93,6 +97,7 @@ export type RegisterResolverInstruction<
export type RegisterResolverInstructionData = { export type RegisterResolverInstructionData = {
discriminator: ReadonlyUint8Array; discriminator: ReadonlyUint8Array;
resolverType: ResolverType; resolverType: ResolverType;
acceptancePolicy: AcceptancePolicy;
name: string; name: string;
description: string; description: string;
feeBps: number; feeBps: number;
@@ -102,6 +107,7 @@ export type RegisterResolverInstructionData = {
export type RegisterResolverInstructionDataArgs = { export type RegisterResolverInstructionDataArgs = {
resolverType: ResolverTypeArgs; resolverType: ResolverTypeArgs;
acceptancePolicy: AcceptancePolicyArgs;
name: string; name: string;
description: string; description: string;
feeBps: number; feeBps: number;
@@ -114,6 +120,7 @@ export function getRegisterResolverInstructionDataEncoder(): Encoder<RegisterRes
getStructEncoder([ getStructEncoder([
["discriminator", fixEncoderSize(getBytesEncoder(), 8)], ["discriminator", fixEncoderSize(getBytesEncoder(), 8)],
["resolverType", getResolverTypeEncoder()], ["resolverType", getResolverTypeEncoder()],
["acceptancePolicy", getAcceptancePolicyEncoder()],
["name", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())], ["name", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
["description", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())], ["description", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
["feeBps", getU16Encoder()], ["feeBps", getU16Encoder()],
@@ -128,6 +135,7 @@ export function getRegisterResolverInstructionDataDecoder(): Decoder<RegisterRes
return getStructDecoder([ return getStructDecoder([
["discriminator", fixDecoderSize(getBytesDecoder(), 8)], ["discriminator", fixDecoderSize(getBytesDecoder(), 8)],
["resolverType", getResolverTypeDecoder()], ["resolverType", getResolverTypeDecoder()],
["acceptancePolicy", getAcceptancePolicyDecoder()],
["name", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())], ["name", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
["description", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())], ["description", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
["feeBps", getU16Decoder()], ["feeBps", getU16Decoder()],
@@ -155,6 +163,7 @@ export type RegisterResolverAsyncInput<
resolverEntry?: Address<TAccountResolverEntry>; resolverEntry?: Address<TAccountResolverEntry>;
systemProgram?: Address<TAccountSystemProgram>; systemProgram?: Address<TAccountSystemProgram>;
resolverType: RegisterResolverInstructionDataArgs["resolverType"]; resolverType: RegisterResolverInstructionDataArgs["resolverType"];
acceptancePolicy: RegisterResolverInstructionDataArgs["acceptancePolicy"];
name: RegisterResolverInstructionDataArgs["name"]; name: RegisterResolverInstructionDataArgs["name"];
description: RegisterResolverInstructionDataArgs["description"]; description: RegisterResolverInstructionDataArgs["description"];
feeBps: RegisterResolverInstructionDataArgs["feeBps"]; feeBps: RegisterResolverInstructionDataArgs["feeBps"];
@@ -242,6 +251,7 @@ export type RegisterResolverInput<
resolverEntry: Address<TAccountResolverEntry>; resolverEntry: Address<TAccountResolverEntry>;
systemProgram?: Address<TAccountSystemProgram>; systemProgram?: Address<TAccountSystemProgram>;
resolverType: RegisterResolverInstructionDataArgs["resolverType"]; resolverType: RegisterResolverInstructionDataArgs["resolverType"];
acceptancePolicy: RegisterResolverInstructionDataArgs["acceptancePolicy"];
name: RegisterResolverInstructionDataArgs["name"]; name: RegisterResolverInstructionDataArgs["name"];
description: RegisterResolverInstructionDataArgs["description"]; description: RegisterResolverInstructionDataArgs["description"];
feeBps: RegisterResolverInstructionDataArgs["feeBps"]; feeBps: RegisterResolverInstructionDataArgs["feeBps"];

View File

@@ -0,0 +1,42 @@
/**
* This code was AUTOGENERATED using the Codama library.
* Please DO NOT EDIT THIS FILE, instead use visitors
* to add features, then rerun Codama to update it.
*
* @see https://github.com/codama-idl/codama
*/
import {
combineCodec,
getEnumDecoder,
getEnumEncoder,
type FixedSizeCodec,
type FixedSizeDecoder,
type FixedSizeEncoder,
} from "@solana/kit";
export enum AcceptancePolicy {
Open,
SignatureGated,
ProgramGated,
}
export type AcceptancePolicyArgs = AcceptancePolicy;
export function getAcceptancePolicyEncoder(): FixedSizeEncoder<AcceptancePolicyArgs> {
return getEnumEncoder(AcceptancePolicy);
}
export function getAcceptancePolicyDecoder(): FixedSizeDecoder<AcceptancePolicy> {
return getEnumDecoder(AcceptancePolicy);
}
export function getAcceptancePolicyCodec(): FixedSizeCodec<
AcceptancePolicyArgs,
AcceptancePolicy
> {
return combineCodec(
getAcceptancePolicyEncoder(),
getAcceptancePolicyDecoder(),
);
}

View File

@@ -6,5 +6,6 @@
* @see https://github.com/codama-idl/codama * @see https://github.com/codama-idl/codama
*/ */
export * from "./acceptancePolicy";
export * from "./resolverType"; export * from "./resolverType";
export * from "./ruling"; export * from "./ruling";

View File

@@ -7,6 +7,98 @@
"description": "Created with Anchor" "description": "Created with Anchor"
}, },
"instructions": [ "instructions": [
{
"name": "buyer_create_escrow",
"discriminator": [
79,
199,
251,
30,
163,
86,
13,
73
],
"accounts": [
{
"name": "buyer",
"writable": true,
"signer": true
},
{
"name": "seller"
},
{
"name": "escrow_account",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
101,
115,
99,
114,
111,
119
]
},
{
"kind": "account",
"path": "seller"
},
{
"kind": "arg",
"path": "escrow_id"
}
]
}
},
{
"name": "vault",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
118,
97,
117,
108,
116
]
},
{
"kind": "account",
"path": "escrow_account"
}
]
}
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
}
],
"args": [
{
"name": "amount",
"type": "u64"
},
{
"name": "dispute_resolver",
"type": {
"option": "pubkey"
}
},
{
"name": "escrow_id",
"type": "u64"
}
]
},
{ {
"name": "cancel", "name": "cancel",
"discriminator": [ "discriminator": [
@@ -21,10 +113,14 @@
], ],
"accounts": [ "accounts": [
{ {
"name": "seller", "name": "canceller",
"writable": true, "writable": true,
"signer": true "signer": true
}, },
{
"name": "buyer",
"writable": true
},
{ {
"name": "escrow_account", "name": "escrow_account",
"writable": true, "writable": true,
@@ -54,6 +150,28 @@
] ]
} }
}, },
{
"name": "vault",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
118,
97,
117,
108,
116
]
},
{
"kind": "account",
"path": "escrow_account"
}
]
}
},
{ {
"name": "system_program", "name": "system_program",
"address": "11111111111111111111111111111111" "address": "11111111111111111111111111111111"
@@ -161,6 +279,9 @@
{ {
"name": "buyer" "name": "buyer"
}, },
{
"name": "resolver"
},
{ {
"name": "escrow_account", "name": "escrow_account",
"writable": true, "writable": true,
@@ -210,6 +331,9 @@
] ]
} }
}, },
{
"name": "resolver_entry"
},
{ {
"name": "system_program", "name": "system_program",
"address": "11111111111111111111111111111111" "address": "11111111111111111111111111111111"
@@ -357,6 +481,99 @@
], ],
"args": [] "args": []
}, },
{
"name": "emergency_resolve",
"discriminator": [
63,
112,
185,
42,
47,
61,
232,
79
],
"accounts": [
{
"name": "buyer",
"signer": true
},
{
"name": "seller",
"writable": true,
"signer": true
},
{
"name": "winner",
"writable": true
},
{
"name": "escrow_account",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
101,
115,
99,
114,
111,
119
]
},
{
"kind": "account",
"path": "escrow_account.seller",
"account": "EscrowAccount"
},
{
"kind": "account",
"path": "escrow_account.escrow_id",
"account": "EscrowAccount"
}
]
}
},
{
"name": "vault",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
118,
97,
117,
108,
116
]
},
{
"kind": "account",
"path": "escrow_account"
}
]
}
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
}
],
"args": [
{
"name": "winner",
"type": {
"defined": {
"name": "Winner"
}
}
}
]
},
{ {
"name": "resolve", "name": "resolve",
"discriminator": [ "discriminator": [
@@ -542,6 +759,60 @@
} }
} }
] ]
},
{
"name": "seller_confirm",
"discriminator": [
21,
244,
3,
109,
138,
95,
198,
242
],
"accounts": [
{
"name": "seller",
"signer": true
},
{
"name": "resolver"
},
{
"name": "escrow_account",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
101,
115,
99,
114,
111,
119
]
},
{
"kind": "account",
"path": "seller"
},
{
"kind": "account",
"path": "escrow_account.escrow_id",
"account": "EscrowAccount"
}
]
}
},
{
"name": "resolver_entry"
}
],
"args": []
} }
], ],
"accounts": [ "accounts": [
@@ -584,6 +855,26 @@
"code": 6004, "code": 6004,
"name": "Expired", "name": "Expired",
"msg": "Escrow has expired" "msg": "Escrow has expired"
},
{
"code": 6005,
"name": "DisputeTimeoutNotReached",
"msg": "Dispute timeout period has not elapsed yet"
},
{
"code": 6006,
"name": "InsufficientVaultBalance",
"msg": "Vault balance is insufficient for the requested escrow amount"
},
{
"code": 6007,
"name": "AmountBelowRentMinimum",
"msg": "Amount must be at least rent-exempt minimum for the vault (~890880 lamports)"
},
{
"code": 6008,
"name": "ResolverSignatureRequired",
"msg": "Resolver must co-sign this transaction (SignatureGated acceptance policy)"
} }
], ],
"types": [ "types": [
@@ -629,6 +920,12 @@
{ {
"name": "escrow_id", "name": "escrow_id",
"type": "u64" "type": "u64"
},
{
"name": "dispute_raised_at",
"type": {
"option": "i64"
}
} }
] ]
} }
@@ -641,6 +938,9 @@
{ {
"name": "AwaitingDeposit" "name": "AwaitingDeposit"
}, },
{
"name": "AwaitingSellerConfirm"
},
{ {
"name": "Active" "name": "Active"
}, },

View File

@@ -64,6 +64,14 @@
} }
} }
}, },
{
"name": "acceptance_policy",
"type": {
"defined": {
"name": "AcceptancePolicy"
}
}
},
{ {
"name": "name", "name": "name",
"type": "string" "type": "string"
@@ -285,6 +293,23 @@
} }
], ],
"types": [ "types": [
{
"name": "AcceptancePolicy",
"type": {
"kind": "enum",
"variants": [
{
"name": "Open"
},
{
"name": "SignatureGated"
},
{
"name": "ProgramGated"
}
]
}
},
{ {
"name": "ResolverEntry", "name": "ResolverEntry",
"type": { "type": {
@@ -302,6 +327,14 @@
} }
} }
}, },
{
"name": "acceptance_policy",
"type": {
"defined": {
"name": "AcceptancePolicy"
}
}
},
{ {
"name": "name", "name": "name",
"type": "string" "type": "string"

View File

@@ -24,8 +24,20 @@ export {
} from "./generated/descro/src/generated/instructions/complete"; } from "./generated/descro/src/generated/instructions/complete";
export { export {
getCancelInstruction, getCancelInstruction,
getCancelInstructionAsync,
type CancelInput, type CancelInput,
type CancelAsyncInput,
} from "./generated/descro/src/generated/instructions/cancel"; } from "./generated/descro/src/generated/instructions/cancel";
export {
getBuyerCreateEscrowInstructionAsync,
getBuyerCreateEscrowInstruction,
type BuyerCreateEscrowAsyncInput,
type BuyerCreateEscrowInput,
} from "./generated/descro/src/generated/instructions/buyerCreateEscrow";
export {
getSellerConfirmInstruction,
type SellerConfirmInput,
} from "./generated/descro/src/generated/instructions/sellerConfirm";
export { export {
getDisputeInstruction, getDisputeInstruction,
type DisputeInput, type DisputeInput,
@@ -43,4 +55,5 @@ export {
type RegisterResolverInput, type RegisterResolverInput,
} from "./generated/descro_ext_resolvers/src/generated/instructions/registerResolver"; } from "./generated/descro_ext_resolvers/src/generated/instructions/registerResolver";
export { Winner } from "./generated/descro/src/generated/types/winner"; export { Winner } from "./generated/descro/src/generated/types/winner";
export { ResolverType } from "./generated/descro_ext_resolvers/src/generated/types/resolverType"; export { ResolverType } from "./generated/descro_ext_resolvers/src/generated/types/resolverType";
export { AcceptancePolicy } from "./generated/descro_ext_resolvers/src/generated/types/acceptancePolicy";

View File

@@ -32,6 +32,9 @@ export type ResolverEntryWithPda = { pda: Address; account: ResolverEntry };
export function isAwaitingDeposit(state: EscrowState): boolean { export function isAwaitingDeposit(state: EscrowState): boolean {
return state === EscrowState.AwaitingDeposit; return state === EscrowState.AwaitingDeposit;
} }
export function isAwaitingSellerConfirm(state: EscrowState): boolean {
return state === EscrowState.AwaitingSellerConfirm;
}
export function isActive(state: EscrowState): boolean { export function isActive(state: EscrowState): boolean {
return state === EscrowState.Active; return state === EscrowState.Active;
} }
@@ -48,6 +51,7 @@ export function isCancelled(state: EscrowState): boolean {
export function escrowStateLabel(state: EscrowState): string { export function escrowStateLabel(state: EscrowState): string {
switch (state) { switch (state) {
case EscrowState.AwaitingDeposit: return "AwaitingDeposit"; case EscrowState.AwaitingDeposit: return "AwaitingDeposit";
case EscrowState.AwaitingSellerConfirm: return "AwaitingSellerConfirm";
case EscrowState.Active: return "Active"; case EscrowState.Active: return "Active";
case EscrowState.Disputed: return "Disputed"; case EscrowState.Disputed: return "Disputed";
case EscrowState.Complete: return "Complete"; case EscrowState.Complete: return "Complete";

3480
yarn.lock

File diff suppressed because it is too large Load Diff