Compare commits
11 Commits
c08e380929
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c203ead1b | ||
|
|
8920463fdd | ||
|
|
e5a0e74ff2 | ||
|
|
e76f5dc565 | ||
|
|
1dd85b5b9c | ||
|
|
5da7fb2b97 | ||
|
|
f97792f190 | ||
|
|
9d9c7a8a43 | ||
|
|
dd5ec25ff9 | ||
|
|
9900ada281 | ||
|
|
0b00030167 |
@@ -1 +1,6 @@
|
||||
approvedGitRepositories:
|
||||
- "**"
|
||||
|
||||
enableScripts: true
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
97
Cargo.lock
generated
97
Cargo.lock
generated
@@ -1076,7 +1076,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anchor-lang",
|
||||
"descro_ext_resolvers",
|
||||
"litesvm",
|
||||
"litesvm 0.12.0",
|
||||
"solana-keypair",
|
||||
"solana-message",
|
||||
"solana-signer",
|
||||
@@ -1088,7 +1088,7 @@ name = "descro_ext_resolvers"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anchor-lang",
|
||||
"litesvm",
|
||||
"litesvm 0.10.0",
|
||||
"solana-keypair",
|
||||
"solana-message",
|
||||
"solana-signer",
|
||||
@@ -1686,7 +1686,7 @@ dependencies = [
|
||||
"solana-precompile-error",
|
||||
"solana-program-error",
|
||||
"solana-program-runtime",
|
||||
"solana-rent",
|
||||
"solana-rent 3.1.0",
|
||||
"solana-sdk-ids",
|
||||
"solana-sha256-hasher",
|
||||
"solana-signature",
|
||||
@@ -1708,6 +1708,70 @@ dependencies = [
|
||||
"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]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
@@ -2721,9 +2785,17 @@ version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75ca9b5cbb6f500f7fd73db5bd95640f71a83f04d6121a0e59a43b202dca2731"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"solana-account",
|
||||
"solana-account-info",
|
||||
"solana-instruction",
|
||||
"solana-program-error",
|
||||
"solana-pubkey 4.2.0",
|
||||
"solana-rent 4.2.0",
|
||||
"solana-sdk-ids",
|
||||
"solana-system-interface 3.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3094,7 +3166,7 @@ dependencies = [
|
||||
"solana-loader-v3-interface",
|
||||
"solana-program-entrypoint",
|
||||
"solana-pubkey 3.0.0",
|
||||
"solana-rent",
|
||||
"solana-rent 3.1.0",
|
||||
"solana-sbpf",
|
||||
"solana-sdk-ids",
|
||||
"solana-slot-hashes",
|
||||
@@ -3145,6 +3217,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "solana-sanitize"
|
||||
version = "3.0.1"
|
||||
@@ -3488,7 +3569,7 @@ dependencies = [
|
||||
"solana-program-error",
|
||||
"solana-program-memory",
|
||||
"solana-pubkey 4.2.0",
|
||||
"solana-rent",
|
||||
"solana-rent 3.1.0",
|
||||
"solana-sdk-ids",
|
||||
"solana-sdk-macro",
|
||||
"solana-slot-hashes",
|
||||
@@ -3540,7 +3621,7 @@ dependencies = [
|
||||
"solana-instruction",
|
||||
"solana-instructions-sysvar",
|
||||
"solana-pubkey 3.0.0",
|
||||
"solana-rent",
|
||||
"solana-rent 3.1.0",
|
||||
"solana-sbpf",
|
||||
"solana-sdk-ids",
|
||||
]
|
||||
@@ -3575,7 +3656,7 @@ dependencies = [
|
||||
"solana-instruction",
|
||||
"solana-instruction-error",
|
||||
"solana-pubkey 3.0.0",
|
||||
"solana-rent",
|
||||
"solana-rent 3.1.0",
|
||||
"solana-sdk-ids",
|
||||
"solana-serde-varint",
|
||||
"solana-serialize-utils",
|
||||
@@ -3605,7 +3686,7 @@ dependencies = [
|
||||
"solana-packet",
|
||||
"solana-program-runtime",
|
||||
"solana-pubkey 3.0.0",
|
||||
"solana-rent",
|
||||
"solana-rent 3.1.0",
|
||||
"solana-sdk-ids",
|
||||
"solana-signer",
|
||||
"solana-slot-hashes",
|
||||
|
||||
34
README.md
34
README.md
@@ -1,6 +1,20 @@
|
||||
# descro
|
||||
|
||||
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. 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, whether that's 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
|
||||
|
||||
@@ -16,7 +30,7 @@ PROTOCOL LAYER
|
||||
└─────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
The protocol only defines the interface. Any resolver that implements it is compatible — from a single authority wallet to a jury DAO voting contract.
|
||||
The protocol only defines the interface. Any resolver that implements it is compatible, from a single authority wallet to a jury DAO voting contract.
|
||||
|
||||
## Programs
|
||||
|
||||
@@ -29,8 +43,8 @@ The protocol only defines the interface. Any resolver that implements it is comp
|
||||
|
||||
Holds trade state and SOL. Two PDAs per escrow:
|
||||
|
||||
- **EscrowAccount** `["escrow", seller_pubkey, escrow_id_le_bytes]` — trade state
|
||||
- **Vault** `["vault", escrow_account_pubkey]` — system-owned, holds SOL only
|
||||
- **EscrowAccount** `["escrow", seller_pubkey, escrow_id_le_bytes]`: trade state
|
||||
- **Vault** `["vault", escrow_account_pubkey]`: system-owned, holds SOL only
|
||||
|
||||
**Instruction flow:**
|
||||
|
||||
@@ -43,7 +57,7 @@ create_escrow (seller)
|
||||
cancel (seller, only from AwaitingDeposit)
|
||||
```
|
||||
|
||||
The `dispute_resolver` field in the escrow can be any pubkey — a wallet signing directly or a program calling `resolve()` via CPI. The check is identical either way.
|
||||
The `dispute_resolver` field in the escrow can be any pubkey, whether that's a wallet signing directly or a program calling `resolve()` via CPI. The check is identical either way.
|
||||
|
||||
**Emergency resolve:** After 14 days in `Disputed` state with no resolution, either party can call `emergency_resolve` to unlock funds.
|
||||
|
||||
@@ -51,7 +65,7 @@ The `dispute_resolver` field in the escrow can be any pubkey — a wallet signin
|
||||
|
||||
An on-chain directory for resolvers. Resolvers register with metadata (name, type, fee, URI); resolution stats are auto-incremented by the escrow program via CPI and cannot be manipulated externally.
|
||||
|
||||
`update_stats` is callable only by the escrow program — enforced via an `["escrow_authority"]` PDA that only the escrow program can sign for.
|
||||
`update_stats` is callable only by the escrow program, enforced via an `["escrow_authority"]` PDA that only the escrow program can sign for.
|
||||
|
||||
Resolver types: `CentralAuthority`, `JuryDAO`, `MAD`, `Algorithmic`, `Multisig`
|
||||
|
||||
@@ -86,7 +100,7 @@ app/ — Next.js + Tamagui playground
|
||||
|
||||
### Build
|
||||
|
||||
Programs must be built in order — the escrow program depends on the registry:
|
||||
Programs must be built in order, since the escrow program depends on the registry:
|
||||
|
||||
```bash
|
||||
cargo build-sbf --manifest-path programs/descro_ext_resolvers/Cargo.toml
|
||||
@@ -123,7 +137,7 @@ yarn workspace descro-app run dev
|
||||
|
||||
## SDK Usage
|
||||
|
||||
The SDK is consumed as raw TypeScript via `transpilePackages` in the Next.js config — no build step needed.
|
||||
The SDK is consumed as raw TypeScript via `transpilePackages` in the Next.js config, so no build step is needed.
|
||||
|
||||
```ts
|
||||
import { DescroSdk } from "@descro/sdk";
|
||||
@@ -153,6 +167,6 @@ sdk.listener.onEscrowUpdate(escrowPubkey, (account) => { ... });
|
||||
## Architecture Notes
|
||||
|
||||
- **Anchor 1.0.x:** `UncheckedAccount<'info>` with `/// CHECK:` doc comments; `#[derive(InitSpace)]` + `#[max_len(N)]` for strings.
|
||||
- **`@coral-xyz/anchor@0.32.x`:** Use `new Program(idl, provider)` — the IDL's `address` field provides the program ID. The 3-argument form is broken in 0.32.x.
|
||||
- **Yarn workspaces:** `nodeLinker: node-modules` (not PnP) — required for Next.js Turbopack compatibility.
|
||||
- **`@coral-xyz/anchor@0.32.x`:** Use `new Program(idl, provider)`. The IDL's `address` field provides the program ID. The 3-argument form is broken in 0.32.x.
|
||||
- **Yarn workspaces:** `nodeLinker: node-modules` (not PnP), required for Next.js Turbopack compatibility.
|
||||
- **LiteSVM tests:** each `tests/common/mod.rs` provides `setup()`, PDA helpers, `send()`/`try_send()`, `ix_*` builders, and `read_*` deserializers. `test_resolve_with_registry.rs` loads both `.so` files to test the full CPI flow.
|
||||
|
||||
@@ -10,19 +10,20 @@
|
||||
"@descro/sdk": "workspace:*",
|
||||
"@solana/connector": "^0.2.4",
|
||||
"@solana/connector-debugger": "^0.1.1",
|
||||
"@solana/kit": "^6.0.0",
|
||||
"@tamagui/config": "^2.0.0-rc.42",
|
||||
"@tamagui/next-theme": "^2.0.0-rc.42",
|
||||
"next": "^16.2.6",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"@solana/kit": "^6.10.0",
|
||||
"@solana/web3.js": "^1.98.4",
|
||||
"@tamagui/config": "^2.3.3",
|
||||
"@tamagui/next-theme": "^2.3.3",
|
||||
"next": "^16.2.9",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-native-web": "^0.21.2",
|
||||
"tamagui": "^2.0.0-rc.42"
|
||||
"tamagui": "^2.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tamagui/cli": "^2.0.0-rc.42",
|
||||
"@types/node": "^25.9.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@tamagui/cli": "^2.3.3",
|
||||
"@types/node": "^26.0.1",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
|
||||
@@ -14,11 +14,13 @@ import {
|
||||
getSignatureFromTransaction,
|
||||
address as kitAddress,
|
||||
} from '@solana/kit'
|
||||
import { useKitTransactionSigner, useWallet, useCluster, useSolanaClient } from '@solana/connector/react'
|
||||
import { getCreateEscrowInstructionAsync } from '@descro/sdk'
|
||||
import { useKitTransactionSigner, useWallet, useSolanaClient } from '@solana/connector/react'
|
||||
import { getCreateEscrowInstructionAsync, getBuyerCreateEscrowInstructionAsync, findResolverEntryPda } from '@descro/sdk'
|
||||
import { lamportsFromSol } from '@/util/lamports'
|
||||
import type { Address } from '@descro/sdk'
|
||||
|
||||
const SYSTEM_PROGRAM_ADDRESS = '11111111111111111111111111111111' as Address
|
||||
|
||||
function getNextEscrowId(walletAddress: string): bigint {
|
||||
const key = `descro_escrow_id_${walletAddress}`
|
||||
const stored = localStorage.getItem(key)
|
||||
@@ -28,6 +30,15 @@ function getNextEscrowId(walletAddress: string): bigint {
|
||||
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 {
|
||||
onCreated?: () => void
|
||||
}
|
||||
@@ -37,7 +48,8 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
|
||||
const { account: address } = useWallet()
|
||||
const { client } = useSolanaClient()
|
||||
|
||||
const [buyer, setBuyer] = useState('')
|
||||
const [mode, setMode] = useState<'seller' | 'buyer'>('seller')
|
||||
const [counterparty, setCounterparty] = useState('')
|
||||
const [amountSol, setAmountSol] = useState('')
|
||||
const [resolver, setResolver] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -54,11 +66,11 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
|
||||
return
|
||||
}
|
||||
|
||||
let buyerAddress: Address
|
||||
let counterpartyAddress: Address
|
||||
try {
|
||||
buyerAddress = kitAddress(buyer)
|
||||
counterpartyAddress = kitAddress(counterparty)
|
||||
} catch {
|
||||
setError('Invalid buyer public key.')
|
||||
setError(`Invalid ${mode === 'seller' ? 'buyer' : 'seller'} public key.`)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -68,7 +80,6 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
|
||||
return
|
||||
}
|
||||
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) {
|
||||
setError('Amount too small: minimum is ~0.00089 SOL (vault must be rent-exempt).')
|
||||
return
|
||||
@@ -87,15 +98,37 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
|
||||
setLoading(true)
|
||||
try {
|
||||
const { rpc, rpcSubscriptions } = client
|
||||
const escrowId = getNextEscrowId(address)
|
||||
|
||||
const ix = await getCreateEscrowInstructionAsync({
|
||||
seller: signer,
|
||||
buyer: buyerAddress,
|
||||
amount: amountLamports,
|
||||
disputeResolver: resolverAddress,
|
||||
escrowId,
|
||||
})
|
||||
let ix: unknown
|
||||
let escrowIdLabel: bigint
|
||||
|
||||
if (mode === 'seller') {
|
||||
const escrowId = getNextEscrowId(address)
|
||||
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()
|
||||
|
||||
@@ -108,11 +141,11 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
|
||||
|
||||
const signed = await signTransactionMessageWithSigners(txMsg)
|
||||
assertIsTransactionWithBlockhashLifetime(signed)
|
||||
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signed, { commitment: 'confirmed' })
|
||||
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions: rpcSubscriptions as never })(signed, { commitment: 'confirmed' })
|
||||
const sig = getSignatureFromTransaction(signed)
|
||||
|
||||
setSuccess(`Escrow #${escrowId} created! Tx: ${sig.slice(0, 16)}…`)
|
||||
setBuyer('')
|
||||
setSuccess(`Escrow #${escrowIdLabel} created! Tx: ${sig.slice(0, 16)}…`)
|
||||
setCounterparty('')
|
||||
setAmountSol('')
|
||||
setResolver('')
|
||||
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 (
|
||||
<YStack gap="$3">
|
||||
<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 }}>
|
||||
<YStack gap="$1">
|
||||
<Text fontSize={13} color="$color11">Buyer Public Key *</Text>
|
||||
<Text fontSize={13} color="$color11">{counterpartyLabel} *</Text>
|
||||
<Input
|
||||
value={buyer}
|
||||
onChangeText={setBuyer}
|
||||
value={counterparty}
|
||||
onChangeText={setCounterparty}
|
||||
placeholder="Pubkey..."
|
||||
style={{ fontFamily: 'monospace' }}
|
||||
fontSize={13}
|
||||
@@ -178,7 +241,7 @@ export function CreateEscrowForm({ onCreated }: CreateEscrowFormProps) {
|
||||
disabled={loading || !address}
|
||||
icon={loading ? <Spinner /> : undefined}
|
||||
>
|
||||
{loading ? 'Creating…' : 'Create Escrow'}
|
||||
{loading ? 'Creating…' : buttonLabel}
|
||||
</Button>
|
||||
</form>
|
||||
</YStack>
|
||||
|
||||
@@ -4,6 +4,7 @@ import React from 'react'
|
||||
import { YStack, XStack, Text } from 'tamagui'
|
||||
import { StatusBadge } from './StatusBadge'
|
||||
import type { EscrowAccountWithPda } from '@descro/sdk'
|
||||
import { lamports } from '@solana/kit'
|
||||
import { lamportsToSolString } from '@/util/lamports'
|
||||
function truncAddr(addr: string): string {
|
||||
return `${addr.slice(0, 4)}…${addr.slice(-4)}`
|
||||
@@ -17,7 +18,7 @@ interface EscrowCardProps {
|
||||
|
||||
export function EscrowCard({ item, selected, onSelect }: EscrowCardProps) {
|
||||
const { pda, account } = item
|
||||
const solAmount = lamportsToSolString(account.amount, 4)
|
||||
const solAmount = lamportsToSolString(lamports(account.amount), 4)
|
||||
|
||||
return (
|
||||
<YStack
|
||||
|
||||
@@ -13,18 +13,21 @@ import {
|
||||
assertIsTransactionWithBlockhashLifetime,
|
||||
getSignatureFromTransaction,
|
||||
isSome,
|
||||
lamports,
|
||||
} from '@solana/kit'
|
||||
import { useKitTransactionSigner, useWallet, useSolanaClient } from '@solana/connector/react'
|
||||
import { StatusBadge } from './StatusBadge'
|
||||
import {
|
||||
isAwaitingDeposit,
|
||||
isAwaitingSellerConfirm,
|
||||
isActive,
|
||||
isDisputed,
|
||||
getDepositInstructionAsync,
|
||||
getCancelInstruction,
|
||||
getCancelInstructionAsync,
|
||||
getCompleteInstructionAsync,
|
||||
getDisputeInstruction,
|
||||
getResolveInstructionAsync,
|
||||
getSellerConfirmInstruction,
|
||||
findResolverEntryPda,
|
||||
DESCRO_EXT_RESOLVERS_PROGRAM_ADDRESS,
|
||||
Winner,
|
||||
@@ -51,7 +54,7 @@ export function EscrowDetail({ item, onAction }: EscrowDetailProps) {
|
||||
const [error, setError] = 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 isBuyer = address != null && address === escrow.buyer
|
||||
const resolverAddr = isSome(escrow.disputeResolver) ? escrow.disputeResolver.value : null
|
||||
@@ -77,7 +80,7 @@ export function EscrowDetail({ item, onAction }: EscrowDetailProps) {
|
||||
)
|
||||
const signed = await signTransactionMessageWithSigners(txMsg)
|
||||
assertIsTransactionWithBlockhashLifetime(signed)
|
||||
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signed, { commitment: 'confirmed' })
|
||||
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions: rpcSubscriptions as never })(signed, { commitment: 'confirmed' })
|
||||
const sig = getSignatureFromTransaction(signed)
|
||||
setSuccess(`Done! Tx: ${sig.slice(0, 20)}…`)
|
||||
onAction?.()
|
||||
@@ -93,7 +96,22 @@ export function EscrowDetail({ item, onAction }: EscrowDetailProps) {
|
||||
runTx(() => getDepositInstructionAsync({ buyer: signer!, escrowAccount: pda }))
|
||||
|
||||
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 = () =>
|
||||
runTx(() =>
|
||||
@@ -152,7 +170,17 @@ export function EscrowDetail({ item, onAction }: EscrowDetailProps) {
|
||||
Deposit
|
||||
</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}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
getRegisterResolverInstructionAsync,
|
||||
resolverTypeLabel,
|
||||
ResolverType,
|
||||
AcceptancePolicy,
|
||||
} from '@descro/sdk'
|
||||
import type { ResolverEntryWithPda } from '@descro/sdk'
|
||||
|
||||
@@ -49,6 +50,13 @@ export function ResolverPanel() {
|
||||
const [feeRecipient, setFeeRecipient] = useState('')
|
||||
const [metadataUri, setMetadataUri] = useState('')
|
||||
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 [error, setError] = useState<string | null>(null)
|
||||
@@ -110,6 +118,7 @@ export function ResolverPanel() {
|
||||
const ix = await getRegisterResolverInstructionAsync({
|
||||
authority: signer,
|
||||
resolverType,
|
||||
acceptancePolicy: ACCEPTANCE_POLICY_OPTIONS[acceptancePolicyIdx].value,
|
||||
name: name.trim(),
|
||||
description: description.trim(),
|
||||
feeBps: fee,
|
||||
@@ -126,7 +135,7 @@ export function ResolverPanel() {
|
||||
)
|
||||
const signed = await signTransactionMessageWithSigners(txMsg)
|
||||
assertIsTransactionWithBlockhashLifetime(signed)
|
||||
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signed, { commitment: 'confirmed' })
|
||||
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions: rpcSubscriptions as never })(signed, { commitment: 'confirmed' })
|
||||
const sig = getSignatureFromTransaction(signed)
|
||||
|
||||
setSuccess(`Registered! Tx: ${sig.slice(0, 20)}…`)
|
||||
@@ -217,6 +226,19 @@ export function ResolverPanel() {
|
||||
</select>
|
||||
</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">
|
||||
<Text fontSize={13} color="$color11">Fee (BPS)</Text>
|
||||
<Input value={feeBps} onChangeText={setFeeBps} placeholder="0" keyboardType="numeric" fontSize={13} />
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { EscrowState } from '@descro/sdk'
|
||||
|
||||
const STATE_COLORS: Record<string, string> = {
|
||||
AwaitingDeposit: '$blue9',
|
||||
AwaitingSellerConfirm: '$purple9',
|
||||
Active: '$green9',
|
||||
Disputed: '$orange9',
|
||||
Complete: '$gray9',
|
||||
|
||||
@@ -36,7 +36,7 @@ export function useEscrowDetail(pda: Address | null) {
|
||||
if (!cancelled) setLoading(false)
|
||||
})
|
||||
|
||||
const unsub = subscribeEscrow(rpcSubscriptions, pda, (updated) => {
|
||||
const unsub = subscribeEscrow(rpcSubscriptions as never, pda, (updated) => {
|
||||
if (!cancelled) setAccount(updated)
|
||||
})
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
**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.
|
||||
- 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.
|
||||
- `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.
|
||||
@@ -25,7 +26,7 @@
|
||||
- `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
|
||||
|
||||
**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 |
|
||||
|---|---|---|
|
||||
| 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/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/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/create_listing.rs` | Init `ListingAccount` |
|
||||
| Create | `programs/solisting/src/instructions/update_listing.rs` | Mutate price/quantity/currencies |
|
||||
@@ -87,9 +88,9 @@ custom-panic = []
|
||||
anchor-lang = "1.0.2"
|
||||
descro = { path = "../descro", features = ["cpi"] }
|
||||
descro_ext_resolvers = { path = "../descro_ext_resolvers" }
|
||||
# Pyth oracle SDK for on-chain price reads.
|
||||
# Verify latest compatible version at https://crates.io/crates/pyth-sdk-solana
|
||||
pyth-sdk-solana = "0.10"
|
||||
# Pyth oracle SDK for on-chain price reads (Pull Oracle / V2 accounts).
|
||||
# Verify latest compatible version at https://crates.io/crates/pyth-solana-receiver-sdk
|
||||
pyth-solana-receiver-sdk = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
litesvm = "0.10.0"
|
||||
@@ -140,11 +141,32 @@ git commit -m "chore(solisting): add crate skeleton and workspace entry"
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
/// 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)]
|
||||
pub enum Currency {
|
||||
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]
|
||||
@@ -154,14 +176,15 @@ pub struct ListingAccount {
|
||||
/// Sole source of truth for price. All alt_currencies are derived from this via oracle.
|
||||
pub canonical_currency: Currency,
|
||||
/// Price in canonical_currency's smallest unit.
|
||||
pub canonical_amount: u64,
|
||||
/// Other currencies the seller accepts. Amounts are always oracle-computed at create_order
|
||||
/// time — never stored independently. Empty = oracle never needed.
|
||||
pub price: u64,
|
||||
/// Pyth V2 PriceFeedAccount for the canonical currency priced in USD.
|
||||
/// `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)]
|
||||
pub alt_currencies: Vec<Currency>,
|
||||
/// 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>,
|
||||
pub alt_currencies: Vec<AltCurrencyConfig>,
|
||||
/// Resolvers the seller accepts for dispute resolution. Empty = any resolver ok.
|
||||
#[max_len(4)]
|
||||
pub accepted_resolvers: Vec<Pubkey>,
|
||||
@@ -185,7 +208,7 @@ pub struct OrderAccount {
|
||||
pub resolver: Pubkey,
|
||||
/// The currency the buyer chose to pay in.
|
||||
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,
|
||||
/// The descro EscrowAccount PDA for this order.
|
||||
pub escrow_account: Pubkey,
|
||||
@@ -214,9 +237,9 @@ pub enum SolistingError {
|
||||
CurrencyNotAccepted,
|
||||
#[msg("Resolver is not accepted by this listing")]
|
||||
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,
|
||||
#[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,
|
||||
#[msg("Oracle price is unavailable, stale, or has too wide a confidence interval")]
|
||||
OraclePriceUnavailable,
|
||||
@@ -233,102 +256,113 @@ pub enum SolistingError {
|
||||
|
||||
- [ ] **Step 3: Write `oracle.rs`**
|
||||
|
||||
The oracle module reads a Pyth SOL/USD price feed and converts between SOL (Lamports) and a
|
||||
stablecoin (6-decimal SPL token, e.g. USDT/USDC). Stablecoins are treated as exactly $1.00 —
|
||||
no second feed needed for the stable side.
|
||||
The oracle module converts between any two currencies using USD as an intermediate. Each
|
||||
currency has an optional Pyth feed (TOKEN/USD). `None` = treat as $1.00 stablecoin. No feed ID
|
||||
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
|
||||
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::state::Currency;
|
||||
|
||||
/// Maximum age of the oracle price in seconds before it is considered stale.
|
||||
const ORACLE_MAX_AGE_SECS: u64 = 60;
|
||||
const ORACLE_MAX_AGE_SECS: i64 = 60;
|
||||
|
||||
/// Reads the Pyth SOL/USD price feed and returns (price_i64, exponent_i32).
|
||||
/// price_i64 * 10^exponent = USD per SOL.
|
||||
/// Returns OraclePriceUnavailable if the price is stale or the feed account is invalid.
|
||||
fn sol_usd_price(oracle_account: &AccountInfo) -> Result<(i64, i32)> {
|
||||
let feed = load_price_feed_from_account_info(oracle_account)
|
||||
/// Reads any Pyth V2 PriceFeedAccount and returns (price_i64, exponent_i32) where
|
||||
/// `price * 10^exponent` is the USD value of 1 whole token (price in USD per whole unit).
|
||||
/// The caller is responsible for verifying the account key matches listing state.
|
||||
fn read_usd_price(oracle_account: &AccountInfo) -> Result<(i64, i32)> {
|
||||
let data = oracle_account.try_borrow_data()?;
|
||||
let price_update = PriceUpdateV2::try_deserialize(&mut data.as_ref())
|
||||
.map_err(|_| error!(SolistingError::OraclePriceUnavailable))?;
|
||||
let msg = &price_update.price_message;
|
||||
let now = Clock::get()?.unix_timestamp;
|
||||
let price = feed
|
||||
.get_price_no_older_than(now, ORACLE_MAX_AGE_SECS)
|
||||
.ok_or(error!(SolistingError::OraclePriceUnavailable))?;
|
||||
Ok((price.price, price.expo))
|
||||
require!(
|
||||
now - msg.publish_time <= ORACLE_MAX_AGE_SECS,
|
||||
SolistingError::OraclePriceUnavailable
|
||||
);
|
||||
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:
|
||||
/// Sol → Spl (stablecoin 6 decimals): lamports → token_units
|
||||
/// Spl (stablecoin 6 decimals) → Sol: token_units → lamports
|
||||
/// `canonical_oracle` / `target_oracle`: pass `None` when the currency is a USD stablecoin.
|
||||
/// `canonical_currency` / `target_currency`: used only for their `decimals()` value.
|
||||
///
|
||||
/// Formula Sol → Spl (assuming 1 stablecoin = $1, 6 decimals):
|
||||
/// stablecoin_units = lamports * price * 10^expo / 10^9 * 10^6
|
||||
/// = 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.
|
||||
/// Slippage: if |computed − expected_amount| / expected_amount > max_slippage_bps / 10_000,
|
||||
/// returns `SlippageExceeded`. Pass `max_slippage_bps = 0` to skip the check.
|
||||
pub fn convert_with_slippage(
|
||||
oracle_account: &AccountInfo,
|
||||
canonical_oracle: Option<&AccountInfo>,
|
||||
canonical_currency: &Currency,
|
||||
canonical_amount: u64,
|
||||
price: u64,
|
||||
target_oracle: Option<&AccountInfo>,
|
||||
target_currency: &Currency,
|
||||
expected_amount: u64,
|
||||
max_slippage_bps: u16,
|
||||
) -> Result<u64> {
|
||||
let (price_raw, expo) = sol_usd_price(oracle_account)?;
|
||||
require!(price_raw > 0, SolistingError::OraclePriceUnavailable);
|
||||
let (cp_raw, cp_expo) = usd_price(canonical_oracle)?;
|
||||
let (tp_raw, tp_expo) = usd_price(target_oracle)?;
|
||||
|
||||
let computed = match (canonical_currency, target_currency) {
|
||||
(Currency::Sol, Currency::Spl { .. }) => {
|
||||
// lamports → stablecoin units (6 decimals, $1 peg)
|
||||
// = lamports * price_raw / 10^(9 - 6 + (-expo))
|
||||
// = lamports * price_raw / 10^(3 - expo) [expo is negative, so 3 - expo > 3]
|
||||
let shift: u32 = (3i32 - expo) as u32; // expo=-8 → shift=11
|
||||
let intermediate = (canonical_amount as u128)
|
||||
.checked_mul(price_raw as u128)
|
||||
.ok_or(error!(SolistingError::OraclePriceUnavailable))?;
|
||||
let divisor = 10u128.pow(shift);
|
||||
u64::try_from(intermediate / divisor)
|
||||
.map_err(|_| error!(SolistingError::OraclePriceUnavailable))?
|
||||
}
|
||||
(Currency::Spl { .. }, Currency::Sol) => {
|
||||
// stablecoin units (6 decimals, $1 peg) → lamports
|
||||
// = token_units * 10^9 / (price_raw * 10^expo)
|
||||
// = token_units * 10^9 / price_raw * 10^(-expo)
|
||||
// = token_units * 10^(9 + (-expo)) / price_raw
|
||||
// = token_units * 10^(9 - expo) / price_raw [expo negative, so 9 - expo > 9]
|
||||
// Example expo=-8: = token_units * 10^17 / price_raw
|
||||
// Then divide out the stablecoin decimals (6):
|
||||
// = token_units * 10^(9 - expo - 6) / price_raw = token_units * 10^(3 - expo) / price_raw
|
||||
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)),
|
||||
let cd = canonical_currency.decimals() as i32;
|
||||
let td = target_currency.decimals() as i32;
|
||||
|
||||
// shift = cp_expo − tp_expo + td − cd
|
||||
let shift: i32 = cp_expo - tp_expo + td - cd;
|
||||
|
||||
// target = price × cp_raw / tp_raw × 10^shift
|
||||
// Use u128 to avoid overflow. tp_raw and cp_raw are always positive (checked above).
|
||||
let numerator = (price as u128)
|
||||
.checked_mul(cp_raw as u128)
|
||||
.ok_or(error!(SolistingError::OraclePriceUnavailable))?;
|
||||
|
||||
let computed: u64 = if shift >= 0 {
|
||||
let scaled = numerator
|
||||
.checked_mul(10u128.pow(shift as u32))
|
||||
.ok_or(error!(SolistingError::OraclePriceUnavailable))?;
|
||||
u64::try_from(scaled / tp_raw as u128)
|
||||
.map_err(|_| error!(SolistingError::OraclePriceUnavailable))?
|
||||
} else {
|
||||
let divisor = 10u128.pow((-shift) as u32);
|
||||
u64::try_from(numerator / divisor / tp_raw as u128)
|
||||
.map_err(|_| error!(SolistingError::OraclePriceUnavailable))?
|
||||
};
|
||||
|
||||
// Slippage: |computed - expected| / expected <= max_slippage_bps / 10_000
|
||||
if max_slippage_bps > 0 {
|
||||
if max_slippage_bps > 0 && expected_amount > 0 {
|
||||
let tolerance = (expected_amount as u128)
|
||||
.checked_mul(max_slippage_bps as u128)
|
||||
.unwrap_or(u128::MAX)
|
||||
/ 10_000;
|
||||
let diff = if computed > expected_amount {
|
||||
computed as u128 - expected_amount as u128
|
||||
} else {
|
||||
expected_amount as u128 - computed as u128
|
||||
};
|
||||
let diff = computed.abs_diff(expected_amount) as u128;
|
||||
require!(diff <= tolerance, SolistingError::SlippageExceeded);
|
||||
}
|
||||
|
||||
@@ -374,8 +408,8 @@ fn seller_can_create_sol_only_listing() {
|
||||
listing_id,
|
||||
Currency::Sol,
|
||||
price,
|
||||
None, // canonical_oracle: None (no alt currencies)
|
||||
vec![], // no alt_currencies
|
||||
None, // no oracle
|
||||
vec![], // any resolver
|
||||
10, // quantity
|
||||
"ipfs://test".to_string(),
|
||||
@@ -384,39 +418,74 @@ fn seller_can_create_sol_only_listing() {
|
||||
|
||||
let listing = read_listing(&svm, &seller.pubkey(), listing_id);
|
||||
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.price_oracle, None);
|
||||
assert_eq!(listing.canonical_oracle, None);
|
||||
assert_eq!(listing.quantity, 10);
|
||||
assert_eq!(listing.quantity_reserved, 0);
|
||||
assert!(listing.is_active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_listing_with_alt_currency_requires_oracle() {
|
||||
// alt_currencies non-empty but price_oracle = None → should fail
|
||||
fn seller_can_create_listing_with_usdc_alt_stablecoin() {
|
||||
// canonical=Sol, alt=USDC with no oracle (stablecoin $1 peg)
|
||||
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(
|
||||
&seller.pubkey(),
|
||||
42,
|
||||
10,
|
||||
Currency::Sol,
|
||||
1_000_000_000,
|
||||
vec![Currency::Spl { mint: usdt_mint }],
|
||||
None, // oracle missing — must fail
|
||||
Some(sol_usd_feed), // canonical_oracle = SOL/USD feed
|
||||
vec![AltCurrencyConfig {
|
||||
currency: Currency::Spl { mint: usdc_mint, decimals: 6 },
|
||||
usd_oracle: None, // USDC is a stablecoin — no feed needed
|
||||
}],
|
||||
vec![],
|
||||
5,
|
||||
"ipfs://x".to_string(),
|
||||
);
|
||||
let result = try_send(&mut svm, &[ix], &[&seller]);
|
||||
assert!(result.is_err());
|
||||
send(&mut svm, &[ix], &[&seller]);
|
||||
|
||||
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]
|
||||
fn seller_can_update_listing() {
|
||||
let (mut svm, seller, _) = setup();
|
||||
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]);
|
||||
|
||||
let ix_update = ix_update_listing(
|
||||
@@ -424,16 +493,16 @@ fn seller_can_update_listing() {
|
||||
listing_id,
|
||||
Currency::Sol,
|
||||
2_000_000_000,
|
||||
vec![],
|
||||
None,
|
||||
vec![],
|
||||
vec![],
|
||||
20,
|
||||
"ipfs://new".to_string(),
|
||||
);
|
||||
send(&mut svm, &[ix_update], &[&seller]);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -441,7 +510,7 @@ fn seller_can_update_listing() {
|
||||
fn seller_can_close_listing() {
|
||||
let (mut svm, seller, _) = setup();
|
||||
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]);
|
||||
|
||||
let ix_close = ix_close_listing(&seller.pubkey(), listing_id);
|
||||
@@ -489,24 +558,19 @@ pub fn handler(
|
||||
ctx: Context<CreateListing>,
|
||||
listing_id: u64,
|
||||
canonical_currency: Currency,
|
||||
canonical_amount: u64,
|
||||
alt_currencies: Vec<Currency>,
|
||||
price_oracle: Option<Pubkey>,
|
||||
price: u64,
|
||||
canonical_oracle: Option<Pubkey>,
|
||||
alt_currencies: Vec<AltCurrencyConfig>,
|
||||
accepted_resolvers: Vec<Pubkey>,
|
||||
quantity: u32,
|
||||
metadata_uri: String,
|
||||
) -> Result<()> {
|
||||
require!(
|
||||
alt_currencies.is_empty() || price_oracle.is_some(),
|
||||
SolistingError::OracleRequired
|
||||
);
|
||||
|
||||
let listing = &mut ctx.accounts.listing_account;
|
||||
listing.seller = ctx.accounts.seller.key();
|
||||
listing.canonical_currency = canonical_currency;
|
||||
listing.canonical_amount = canonical_amount;
|
||||
listing.price = price;
|
||||
listing.canonical_oracle = canonical_oracle;
|
||||
listing.alt_currencies = alt_currencies;
|
||||
listing.price_oracle = price_oracle;
|
||||
listing.accepted_resolvers = accepted_resolvers;
|
||||
listing.quantity = quantity;
|
||||
listing.quantity_reserved = 0;
|
||||
@@ -542,23 +606,18 @@ pub struct UpdateListing<'info> {
|
||||
pub fn handler(
|
||||
ctx: Context<UpdateListing>,
|
||||
canonical_currency: Currency,
|
||||
canonical_amount: u64,
|
||||
alt_currencies: Vec<Currency>,
|
||||
price_oracle: Option<Pubkey>,
|
||||
price: u64,
|
||||
canonical_oracle: Option<Pubkey>,
|
||||
alt_currencies: Vec<AltCurrencyConfig>,
|
||||
accepted_resolvers: Vec<Pubkey>,
|
||||
quantity: u32,
|
||||
metadata_uri: String,
|
||||
) -> Result<()> {
|
||||
require!(
|
||||
alt_currencies.is_empty() || price_oracle.is_some(),
|
||||
SolistingError::OracleRequired
|
||||
);
|
||||
|
||||
let listing = &mut ctx.accounts.listing_account;
|
||||
listing.canonical_currency = canonical_currency;
|
||||
listing.canonical_amount = canonical_amount;
|
||||
listing.price = price;
|
||||
listing.canonical_oracle = canonical_oracle;
|
||||
listing.alt_currencies = alt_currencies;
|
||||
listing.price_oracle = price_oracle;
|
||||
listing.accepted_resolvers = accepted_resolvers;
|
||||
listing.quantity = quantity;
|
||||
listing.metadata_uri = metadata_uri;
|
||||
@@ -643,9 +702,9 @@ pub fn ix_create_listing(
|
||||
seller: &Pubkey,
|
||||
listing_id: u64,
|
||||
canonical_currency: Currency,
|
||||
canonical_amount: u64,
|
||||
alt_currencies: Vec<Currency>,
|
||||
price_oracle: Option<Pubkey>,
|
||||
price: u64,
|
||||
canonical_oracle: Option<Pubkey>,
|
||||
alt_currencies: Vec<AltCurrencyConfig>,
|
||||
accepted_resolvers: Vec<Pubkey>,
|
||||
quantity: u32,
|
||||
metadata_uri: String,
|
||||
@@ -657,9 +716,9 @@ pub fn ix_update_listing(
|
||||
seller: &Pubkey,
|
||||
listing_id: u64,
|
||||
canonical_currency: Currency,
|
||||
canonical_amount: u64,
|
||||
alt_currencies: Vec<Currency>,
|
||||
price_oracle: Option<Pubkey>,
|
||||
price: u64,
|
||||
canonical_oracle: Option<Pubkey>,
|
||||
alt_currencies: Vec<AltCurrencyConfig>,
|
||||
accepted_resolvers: Vec<Pubkey>,
|
||||
quantity: u32,
|
||||
metadata_uri: String,
|
||||
@@ -712,32 +771,32 @@ pub mod solisting {
|
||||
ctx: Context<CreateListing>,
|
||||
listing_id: u64,
|
||||
canonical_currency: state::Currency,
|
||||
canonical_amount: u64,
|
||||
alt_currencies: Vec<state::Currency>,
|
||||
price_oracle: Option<Pubkey>,
|
||||
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,
|
||||
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>,
|
||||
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,
|
||||
ctx, canonical_currency, price,
|
||||
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
|
||||
// Assert: OrderAccount exists with payment_currency=Sol, amount=0.1 SOL in lamports
|
||||
// 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
|
||||
let (mut svm, seller, buyer) = setup();
|
||||
let listing_id = 1u64;
|
||||
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]);
|
||||
|
||||
let order_id = 1u64;
|
||||
@@ -817,7 +876,7 @@ fn buyer_can_create_order_canonical_sol() {
|
||||
fn create_order_fails_if_listing_inactive() {
|
||||
let (mut svm, seller, buyer) = setup();
|
||||
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]);
|
||||
let ix_close = ix_close_listing(&seller.pubkey(), listing_id);
|
||||
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 listing_id = 3u64;
|
||||
// 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_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();
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -848,12 +907,12 @@ fn create_order_fails_if_currency_not_accepted() {
|
||||
let (mut svm, seller, buyer) = setup();
|
||||
let listing_id = 4u64;
|
||||
// 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]);
|
||||
|
||||
let fake_mint = Pubkey::new_unique();
|
||||
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]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
@@ -863,12 +922,12 @@ fn create_order_fails_if_resolver_not_accepted() {
|
||||
let (mut svm, seller, buyer) = setup();
|
||||
let listing_id = 5u64;
|
||||
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]);
|
||||
|
||||
// Pass a different resolver — must fail
|
||||
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]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
@@ -936,9 +995,14 @@ pub struct CreateOrder<'info> {
|
||||
)]
|
||||
pub descro_vault: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Pyth price feed account. Required when payment_currency != canonical_currency.
|
||||
/// Pass SystemProgram ID as a no-op placeholder when oracle is not needed.
|
||||
pub oracle: UncheckedAccount<'info>,
|
||||
/// CHECK: Pyth USD feed for the canonical currency (listing.canonical_oracle).
|
||||
/// Pass SystemProgram as placeholder when canonical_oracle is None (stablecoin).
|
||||
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
|
||||
pub descro_program: UncheckedAccount<'info>,
|
||||
@@ -972,24 +1036,37 @@ pub fn handler(
|
||||
|
||||
// Determine amount in payment_currency
|
||||
let amount = if payment_currency == listing.canonical_currency {
|
||||
// No oracle needed — pay canonical amount directly
|
||||
listing.canonical_amount
|
||||
// Paying in canonical currency — no oracle needed
|
||||
listing.price
|
||||
} else {
|
||||
// Must be an accepted alt_currency
|
||||
require!(
|
||||
listing.alt_currencies.contains(&payment_currency),
|
||||
SolistingError::CurrencyNotAccepted
|
||||
);
|
||||
// Oracle must be configured on listing
|
||||
let oracle_key = listing.price_oracle.ok_or(error!(SolistingError::OracleRequired))?;
|
||||
require!(
|
||||
ctx.accounts.oracle.key() == oracle_key,
|
||||
SolistingError::OracleMismatch
|
||||
);
|
||||
// Must be a configured alt_currency
|
||||
let alt_cfg = listing.alt_currencies
|
||||
.iter()
|
||||
.find(|c| c.currency == payment_currency)
|
||||
.ok_or(error!(SolistingError::CurrencyNotAccepted))?;
|
||||
|
||||
// Validate passed oracle accounts against listing state
|
||||
if let Some(expected_canonical) = listing.canonical_oracle {
|
||||
require!(
|
||||
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(
|
||||
ctx.accounts.oracle.as_ref(),
|
||||
c_oracle,
|
||||
&listing.canonical_currency,
|
||||
listing.canonical_amount,
|
||||
listing.price,
|
||||
t_oracle,
|
||||
&payment_currency,
|
||||
expected_amount,
|
||||
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()
|
||||
}
|
||||
|
||||
// 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(
|
||||
buyer: &Pubkey,
|
||||
seller: &Pubkey,
|
||||
@@ -1065,13 +1143,14 @@ pub fn ix_create_order(
|
||||
order_id: u64,
|
||||
payment_currency: Currency,
|
||||
max_slippage_bps: u16,
|
||||
oracle: Option<Pubkey>,
|
||||
canonical_oracle: Option<Pubkey>,
|
||||
target_oracle: Option<Pubkey>,
|
||||
) -> solana_message::compiled_instruction::CompiledInstruction {
|
||||
let listing = listing_pda(seller, listing_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 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(
|
||||
@@ -1081,7 +1160,8 @@ pub fn ix_create_order_with_resolver(
|
||||
order_id: u64,
|
||||
payment_currency: Currency,
|
||||
max_slippage_bps: u16,
|
||||
oracle: Option<Pubkey>,
|
||||
canonical_oracle: Option<Pubkey>,
|
||||
target_oracle: Option<Pubkey>,
|
||||
resolver: Pubkey,
|
||||
) -> solana_message::compiled_instruction::CompiledInstruction {
|
||||
let listing = listing_pda(seller, listing_id);
|
||||
@@ -1095,9 +1175,10 @@ pub fn ix_create_order_with_resolver(
|
||||
&[b"vault", escrow_account.as_ref()],
|
||||
&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
|
||||
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 listing_id = 10u64;
|
||||
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;
|
||||
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 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 listing_id = 20u64;
|
||||
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_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None)], &[&buyer]);
|
||||
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, None)], &[&buyer]);
|
||||
|
||||
let buyer_before = svm.get_account(&buyer.pubkey()).map(|a| a.lamports).unwrap_or(0);
|
||||
let listing_key = listing_pda(&seller.pubkey(), listing_id);
|
||||
@@ -1281,8 +1362,8 @@ fn seller_can_reject_order() {
|
||||
fn buyer_can_cancel_order() {
|
||||
let (mut svm, seller, buyer) = setup();
|
||||
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_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None)], &[&buyer]);
|
||||
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, None)], &[&buyer]);
|
||||
|
||||
let listing_key = listing_pda(&seller.pubkey(), listing_id);
|
||||
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
|
||||
let (mut svm, seller, buyer) = setup();
|
||||
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_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None)], &[&buyer]);
|
||||
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, None)], &[&buyer]);
|
||||
|
||||
let listing_key = listing_pda(&seller.pubkey(), listing_id);
|
||||
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() {
|
||||
let (mut svm, seller, buyer) = setup();
|
||||
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_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None)], &[&buyer]);
|
||||
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, None)], &[&buyer]);
|
||||
|
||||
let listing_key = listing_pda(&seller.pubkey(), listing_id);
|
||||
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() {
|
||||
let (mut svm, seller, buyer) = setup();
|
||||
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_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None)], &[&buyer]);
|
||||
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, None)], &[&buyer]);
|
||||
|
||||
let listing_key = listing_pda(&seller.pubkey(), listing_id);
|
||||
let order = read_order(&svm, listing_key, buyer.pubkey(), 1);
|
||||
@@ -1664,12 +1745,12 @@ declare_id!("So1istingProgramID11111111111111111111111111"); // replace with: so
|
||||
pub mod solisting {
|
||||
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<()> {
|
||||
create_listing::handler(ctx, listing_id, canonical_currency, canonical_amount, alt_currencies, price_oracle, accepted_resolvers, quantity, metadata_uri)
|
||||
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, 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<()> {
|
||||
update_listing::handler(ctx, canonical_currency, canonical_amount, alt_currencies, price_oracle, accepted_resolvers, quantity, metadata_uri)
|
||||
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, price, canonical_oracle, alt_currencies, accepted_resolvers, quantity, metadata_uri)
|
||||
}
|
||||
|
||||
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<()> {
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
"lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
|
||||
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anchor-lang/core": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bn.js": "^5.2.0",
|
||||
"@types/chai": "^5.2.3",
|
||||
|
||||
@@ -25,7 +25,7 @@ anchor-lang = "1.0.2"
|
||||
descro_ext_resolvers = { path = "../descro_ext_resolvers", features = ["cpi"] }
|
||||
|
||||
[dev-dependencies]
|
||||
litesvm = "0.10.0"
|
||||
litesvm = "0.12.0"
|
||||
solana-message = "3.0.1"
|
||||
solana-transaction = "3.0.2"
|
||||
solana-signer = "3.0.0"
|
||||
|
||||
@@ -18,4 +18,6 @@ pub enum EscrowError {
|
||||
InsufficientVaultBalance,
|
||||
#[msg("Amount must be at least rent-exempt minimum for the vault (~890880 lamports)")]
|
||||
AmountBelowRentMinimum,
|
||||
#[msg("Resolver must co-sign this transaction (SignatureGated acceptance policy)")]
|
||||
ResolverSignatureRequired,
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
#![allow(ambiguous_glob_reexports)]
|
||||
|
||||
pub mod buyer_create_escrow;
|
||||
pub mod cancel;
|
||||
pub mod complete;
|
||||
pub mod create_escrow;
|
||||
pub mod create_escrow_prefunded;
|
||||
pub mod deposit;
|
||||
pub mod dispute;
|
||||
pub mod emergency_resolve;
|
||||
pub mod resolve;
|
||||
pub mod seller_confirm;
|
||||
|
||||
pub use buyer_create_escrow::*;
|
||||
pub use cancel::*;
|
||||
pub use complete::*;
|
||||
pub use create_escrow::*;
|
||||
pub use create_escrow_prefunded::*;
|
||||
pub use deposit::*;
|
||||
pub use dispute::*;
|
||||
pub use emergency_resolve::*;
|
||||
pub use resolve::*;
|
||||
pub use seller_confirm::*;
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
use anchor_lang::prelude::*;
|
||||
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)]
|
||||
#[instruction(amount: u64, dispute_resolver: Option<Pubkey>, escrow_id: u64)]
|
||||
pub struct CreateEscrowPrefunded<'info> {
|
||||
pub struct BuyerCreateEscrow<'info> {
|
||||
#[account(mut)]
|
||||
pub seller: Signer<'info>,
|
||||
pub buyer: Signer<'info>,
|
||||
|
||||
/// CHECK: Only stored as pubkey, no ownership check needed
|
||||
pub buyer: UncheckedAccount<'info>,
|
||||
pub seller: UncheckedAccount<'info>,
|
||||
|
||||
#[account(
|
||||
init,
|
||||
payer = seller,
|
||||
payer = buyer,
|
||||
space = 8 + EscrowAccount::INIT_SPACE,
|
||||
seeds = [b"escrow", seller.key().as_ref(), &escrow_id.to_le_bytes()],
|
||||
bump
|
||||
)]
|
||||
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(
|
||||
mut,
|
||||
seeds = [b"vault", escrow_account.key().as_ref()],
|
||||
@@ -32,20 +33,31 @@ pub struct CreateEscrowPrefunded<'info> {
|
||||
}
|
||||
|
||||
pub fn handler(
|
||||
ctx: Context<CreateEscrowPrefunded>,
|
||||
ctx: Context<BuyerCreateEscrow>,
|
||||
amount: u64,
|
||||
dispute_resolver: Option<Pubkey>,
|
||||
escrow_id: u64,
|
||||
) -> Result<()> {
|
||||
let vault_lamports = ctx.accounts.vault.to_account_info().lamports();
|
||||
require!(vault_lamports >= amount, EscrowError::InsufficientVaultBalance);
|
||||
let rent_min = Rent::get()?.minimum_balance(0);
|
||||
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;
|
||||
escrow.seller = ctx.accounts.seller.key();
|
||||
escrow.buyer = ctx.accounts.buyer.key();
|
||||
escrow.amount = amount;
|
||||
escrow.dispute_resolver = dispute_resolver;
|
||||
escrow.state = EscrowState::Active;
|
||||
escrow.state = EscrowState::AwaitingSellerConfirm;
|
||||
escrow.bump = ctx.bumps.escrow_account;
|
||||
escrow.vault_bump = ctx.bumps.vault;
|
||||
escrow.escrow_id = escrow_id;
|
||||
@@ -1,25 +1,66 @@
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::system_program::{self, Transfer};
|
||||
use crate::state::{EscrowAccount, EscrowState};
|
||||
use crate::error::EscrowError;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Cancel<'info> {
|
||||
#[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(
|
||||
mut,
|
||||
seeds = [b"escrow", escrow_account.seller.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::AwaitingDeposit @ EscrowError::InvalidState,
|
||||
close = seller,
|
||||
constraint = (
|
||||
canceller.key() == escrow_account.buyer ||
|
||||
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>,
|
||||
|
||||
/// 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 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(())
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ pub struct CreateEscrow<'info> {
|
||||
/// CHECK: Only stored as pubkey, no ownership check needed
|
||||
pub buyer: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Resolver — may need to co-sign if AcceptancePolicy is SignatureGated
|
||||
pub resolver: UncheckedAccount<'info>,
|
||||
|
||||
#[account(
|
||||
init,
|
||||
payer = seller,
|
||||
@@ -28,6 +31,9 @@ pub struct CreateEscrow<'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>,
|
||||
}
|
||||
|
||||
@@ -40,6 +46,23 @@ pub fn handler(
|
||||
let rent_min = Rent::get()?.minimum_balance(0);
|
||||
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;
|
||||
escrow.seller = ctx.accounts.seller.key();
|
||||
escrow.buyer = ctx.accounts.buyer.key();
|
||||
|
||||
47
programs/descro/src/instructions/seller_confirm.rs
Normal file
47
programs/descro/src/instructions/seller_confirm.rs
Normal 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(())
|
||||
}
|
||||
@@ -26,13 +26,13 @@ pub mod descro {
|
||||
create_escrow::handler(ctx, amount, dispute_resolver, escrow_id)
|
||||
}
|
||||
|
||||
pub fn create_escrow_prefunded(
|
||||
ctx: Context<CreateEscrowPrefunded>,
|
||||
pub fn buyer_create_escrow(
|
||||
ctx: Context<BuyerCreateEscrow>,
|
||||
amount: u64,
|
||||
dispute_resolver: Option<Pubkey>,
|
||||
escrow_id: u64,
|
||||
) -> 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<()> {
|
||||
@@ -51,6 +51,10 @@ pub mod descro {
|
||||
resolve::handler(ctx, winner)
|
||||
}
|
||||
|
||||
pub fn seller_confirm(ctx: Context<SellerConfirm>) -> Result<()> {
|
||||
seller_confirm::handler(ctx)
|
||||
}
|
||||
|
||||
pub fn cancel(ctx: Context<Cancel>) -> Result<()> {
|
||||
cancel::handler(ctx)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ pub struct EscrowAccount {
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, InitSpace, Debug)]
|
||||
pub enum EscrowState {
|
||||
AwaitingDeposit,
|
||||
AwaitingSellerConfirm,
|
||||
Active,
|
||||
Disputed,
|
||||
Complete,
|
||||
|
||||
@@ -81,6 +81,24 @@ pub fn ix_create_escrow(
|
||||
amount: u64,
|
||||
dispute_resolver: Option<Pubkey>,
|
||||
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 {
|
||||
let escrow = escrow_pda(seller, escrow_id);
|
||||
let vault = vault_pda(&escrow);
|
||||
@@ -95,8 +113,10 @@ pub fn ix_create_escrow(
|
||||
descro::accounts::CreateEscrow {
|
||||
seller: *seller,
|
||||
buyer: *buyer,
|
||||
resolver: *resolver,
|
||||
escrow_account: escrow,
|
||||
vault,
|
||||
resolver_entry: *resolver_entry,
|
||||
system_program: system_program::ID,
|
||||
}
|
||||
.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 vault = vault_pda(&escrow);
|
||||
Instruction::new_with_bytes(
|
||||
descro::id(),
|
||||
&descro::instruction::Cancel {}.data(),
|
||||
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,
|
||||
escrow_account: escrow,
|
||||
vault,
|
||||
system_program: system_program::ID,
|
||||
}
|
||||
.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 {
|
||||
let escrow = escrow_pda(seller, escrow_id);
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
74
programs/descro/tests/test_buyer_flow.rs
Normal file
74
programs/descro/tests/test_buyer_flow.rs
Normal 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,
|
||||
));
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use common::*;
|
||||
use solana_signer::Signer;
|
||||
|
||||
#[test]
|
||||
fn cancel_returns_rent_to_seller() {
|
||||
fn seller_can_cancel_awaiting_deposit() {
|
||||
let (mut svm, seller, buyer, _resolver) = setup();
|
||||
|
||||
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);
|
||||
|
||||
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());
|
||||
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]
|
||||
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();
|
||||
|
||||
send(
|
||||
@@ -34,7 +87,7 @@ fn cancel_after_deposit_fails() {
|
||||
|
||||
assert!(!try_send(
|
||||
&mut svm,
|
||||
ix_cancel(&seller.pubkey(), ESCROW_ID),
|
||||
ix_cancel(&seller.pubkey(), &buyer.pubkey(), &seller.pubkey(), ESCROW_ID),
|
||||
&seller,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use common::*;
|
||||
use anchor_lang::InstructionData;
|
||||
use anchor_lang::ToAccountMetas;
|
||||
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 litesvm::LiteSVM;
|
||||
use solana_keypair::Keypair;
|
||||
@@ -37,6 +37,7 @@ fn ix_register_resolver(authority: &Pubkey) -> Instruction {
|
||||
descro_ext_resolvers::id(),
|
||||
&descro_ext_resolvers::instruction::RegisterResolver {
|
||||
resolver_type: ResolverType::CentralAuthority,
|
||||
acceptance_policy: AcceptancePolicy::Open,
|
||||
name: "Test Resolver".to_string(),
|
||||
description: "Integration test resolver".to_string(),
|
||||
fee_bps: 100,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anchor_lang::prelude::*;
|
||||
use crate::state::{ResolverEntry, ResolverType};
|
||||
use crate::state::{AcceptancePolicy, ResolverEntry, ResolverType};
|
||||
use crate::error::RegistryError;
|
||||
|
||||
#[derive(Accounts)]
|
||||
@@ -22,6 +22,7 @@ pub struct RegisterResolver<'info> {
|
||||
pub fn handler(
|
||||
ctx: Context<RegisterResolver>,
|
||||
resolver_type: ResolverType,
|
||||
acceptance_policy: AcceptancePolicy,
|
||||
name: String,
|
||||
description: String,
|
||||
fee_bps: u16,
|
||||
@@ -34,6 +35,7 @@ pub fn handler(
|
||||
let entry = &mut ctx.accounts.resolver_entry;
|
||||
entry.authority = ctx.accounts.authority.key();
|
||||
entry.resolver_type = resolver_type;
|
||||
entry.acceptance_policy = acceptance_policy;
|
||||
entry.name = name;
|
||||
entry.description = description;
|
||||
entry.fee_bps = fee_bps;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#![allow(clippy::diverging_sub_expression)]
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
pub mod error;
|
||||
pub mod instructions;
|
||||
@@ -23,6 +24,7 @@ pub mod descro_ext_resolvers {
|
||||
pub fn register_resolver(
|
||||
ctx: Context<RegisterResolver>,
|
||||
resolver_type: ResolverType,
|
||||
acceptance_policy: AcceptancePolicy,
|
||||
name: String,
|
||||
description: String,
|
||||
fee_bps: u16,
|
||||
@@ -32,6 +34,7 @@ pub mod descro_ext_resolvers {
|
||||
register::handler(
|
||||
ctx,
|
||||
resolver_type,
|
||||
acceptance_policy,
|
||||
name,
|
||||
description,
|
||||
fee_bps,
|
||||
|
||||
@@ -5,6 +5,7 @@ use anchor_lang::prelude::*;
|
||||
pub struct ResolverEntry {
|
||||
pub authority: Pubkey,
|
||||
pub resolver_type: ResolverType,
|
||||
pub acceptance_policy: AcceptancePolicy,
|
||||
#[max_len(64)]
|
||||
pub name: String,
|
||||
#[max_len(256)]
|
||||
@@ -28,6 +29,13 @@ pub enum ResolverType {
|
||||
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.
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq)]
|
||||
pub enum Ruling {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![allow(dead_code, unused_imports)]
|
||||
#![allow(dead_code, unused_imports, clippy::too_many_arguments)]
|
||||
|
||||
pub use anchor_lang::prelude::Pubkey;
|
||||
use {
|
||||
@@ -6,7 +6,7 @@ use {
|
||||
solana_program::{instruction::Instruction, system_program},
|
||||
AccountDeserialize, InstructionData, ToAccountMetas,
|
||||
},
|
||||
descro_ext_resolvers::{ResolverEntry, ResolverType},
|
||||
descro_ext_resolvers::{AcceptancePolicy, ResolverEntry, ResolverType},
|
||||
litesvm::LiteSVM,
|
||||
solana_keypair::Keypair,
|
||||
solana_message::{Message, VersionedMessage},
|
||||
@@ -51,6 +51,7 @@ pub fn try_send(svm: &mut LiteSVM, ix: Instruction, payer: &Keypair) -> bool {
|
||||
pub fn ix_register(
|
||||
authority: &Pubkey,
|
||||
resolver_type: ResolverType,
|
||||
acceptance_policy: AcceptancePolicy,
|
||||
name: &str,
|
||||
description: &str,
|
||||
fee_bps: u16,
|
||||
@@ -62,6 +63,7 @@ pub fn ix_register(
|
||||
descro_ext_resolvers::id(),
|
||||
&descro_ext_resolvers::instruction::RegisterResolver {
|
||||
resolver_type,
|
||||
acceptance_policy,
|
||||
name: name.to_string(),
|
||||
description: description.to_string(),
|
||||
fee_bps,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod common;
|
||||
use common::*;
|
||||
use descro_ext_resolvers::ResolverType;
|
||||
use descro_ext_resolvers::{AcceptancePolicy, ResolverType};
|
||||
use solana_keypair::Keypair;
|
||||
use solana_signer::Signer;
|
||||
|
||||
@@ -13,6 +13,7 @@ fn registers_entry_with_correct_state() {
|
||||
ix_register(
|
||||
&authority.pubkey(),
|
||||
ResolverType::CentralAuthority,
|
||||
AcceptancePolicy::Open,
|
||||
"Acme Resolvers",
|
||||
"Fast and fair dispute resolution",
|
||||
100, // 1%
|
||||
@@ -25,6 +26,7 @@ fn registers_entry_with_correct_state() {
|
||||
let entry = read_entry(&svm, &authority.pubkey());
|
||||
assert_eq!(entry.authority, authority.pubkey());
|
||||
assert_eq!(entry.resolver_type, ResolverType::CentralAuthority);
|
||||
assert_eq!(entry.acceptance_policy, AcceptancePolicy::Open);
|
||||
assert_eq!(entry.name, "Acme Resolvers");
|
||||
assert_eq!(entry.fee_bps, 100);
|
||||
assert_eq!(entry.total_resolved, 0);
|
||||
@@ -41,6 +43,7 @@ fn duplicate_register_fails() {
|
||||
ix_register(
|
||||
&authority.pubkey(),
|
||||
ResolverType::CentralAuthority,
|
||||
AcceptancePolicy::Open,
|
||||
"Acme",
|
||||
"",
|
||||
0,
|
||||
@@ -54,6 +57,7 @@ fn duplicate_register_fails() {
|
||||
ix_register(
|
||||
&authority.pubkey(),
|
||||
ResolverType::CentralAuthority,
|
||||
AcceptancePolicy::Open,
|
||||
"Acme Again",
|
||||
"",
|
||||
0,
|
||||
@@ -73,6 +77,7 @@ fn register_empty_name_fails() {
|
||||
ix_register(
|
||||
&authority.pubkey(),
|
||||
ResolverType::CentralAuthority,
|
||||
AcceptancePolicy::Open,
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
@@ -92,6 +97,7 @@ fn register_invalid_fee_bps_fails() {
|
||||
ix_register(
|
||||
&authority.pubkey(),
|
||||
ResolverType::CentralAuthority,
|
||||
AcceptancePolicy::Open,
|
||||
"Acme",
|
||||
"",
|
||||
10_001, // > 100%
|
||||
@@ -110,12 +116,12 @@ fn different_authorities_have_separate_entries() {
|
||||
|
||||
send(
|
||||
&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,
|
||||
);
|
||||
send(
|
||||
&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,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod common;
|
||||
use common::*;
|
||||
use descro_ext_resolvers::ResolverType;
|
||||
use descro_ext_resolvers::{AcceptancePolicy, ResolverType};
|
||||
use solana_keypair::Keypair;
|
||||
use solana_signer::Signer;
|
||||
|
||||
@@ -10,6 +10,7 @@ fn register_default(svm: &mut litesvm::LiteSVM, authority: &Keypair) {
|
||||
ix_register(
|
||||
&authority.pubkey(),
|
||||
ResolverType::CentralAuthority,
|
||||
AcceptancePolicy::Open,
|
||||
"Original Name",
|
||||
"Original description",
|
||||
100,
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
getAddressEncoder,
|
||||
getBytesDecoder,
|
||||
getBytesEncoder,
|
||||
getI64Decoder,
|
||||
getI64Encoder,
|
||||
getOptionDecoder,
|
||||
getOptionEncoder,
|
||||
getStructDecoder,
|
||||
@@ -69,6 +71,7 @@ export type EscrowAccount = {
|
||||
bump: number;
|
||||
vaultBump: number;
|
||||
escrowId: bigint;
|
||||
disputeRaisedAt: Option<bigint>;
|
||||
};
|
||||
|
||||
export type EscrowAccountArgs = {
|
||||
@@ -80,6 +83,7 @@ export type EscrowAccountArgs = {
|
||||
bump: number;
|
||||
vaultBump: number;
|
||||
escrowId: number | bigint;
|
||||
disputeRaisedAt: OptionOrNullable<number | bigint>;
|
||||
};
|
||||
|
||||
/** Gets the encoder for {@link EscrowAccountArgs} account data. */
|
||||
@@ -95,6 +99,7 @@ export function getEscrowAccountEncoder(): Encoder<EscrowAccountArgs> {
|
||||
["bump", getU8Encoder()],
|
||||
["vaultBump", getU8Encoder()],
|
||||
["escrowId", getU64Encoder()],
|
||||
["disputeRaisedAt", getOptionEncoder(getI64Encoder())],
|
||||
]),
|
||||
(value) => ({ ...value, discriminator: ESCROW_ACCOUNT_DISCRIMINATOR }),
|
||||
);
|
||||
@@ -112,6 +117,7 @@ export function getEscrowAccountDecoder(): Decoder<EscrowAccount> {
|
||||
["bump", getU8Decoder()],
|
||||
["vaultBump", getU8Decoder()],
|
||||
["escrowId", getU64Decoder()],
|
||||
["disputeRaisedAt", getOptionDecoder(getI64Decoder())],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,20 +24,36 @@ export const DESCRO_ERROR__NO_RESOLVER_CONFIGURED = 0x1772; // 6002
|
||||
export const DESCRO_ERROR__UNAUTHORIZED_RESOLVER = 0x1773; // 6003
|
||||
/** Expired: Escrow has expired */
|
||||
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 =
|
||||
| typeof DESCRO_ERROR__AMOUNT_BELOW_RENT_MINIMUM
|
||||
| typeof DESCRO_ERROR__DISPUTE_TIMEOUT_NOT_REACHED
|
||||
| typeof DESCRO_ERROR__EXPIRED
|
||||
| typeof DESCRO_ERROR__INSUFFICIENT_VAULT_BALANCE
|
||||
| typeof DESCRO_ERROR__INVALID_STATE
|
||||
| typeof DESCRO_ERROR__NO_RESOLVER_CONFIGURED
|
||||
| typeof DESCRO_ERROR__RESOLVER_SIGNATURE_REQUIRED
|
||||
| typeof DESCRO_ERROR__UNAUTHORIZED
|
||||
| typeof DESCRO_ERROR__UNAUTHORIZED_RESOLVER;
|
||||
|
||||
let descroErrorMessages: Record<DescroError, string> | undefined;
|
||||
if (process.env["NODE_ENV"] !== "production") {
|
||||
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__INSUFFICIENT_VAULT_BALANCE]: `Vault balance is insufficient for the requested escrow amount`,
|
||||
[DESCRO_ERROR__INVALID_STATE]: `Invalid state for this instruction`,
|
||||
[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_RESOLVER]: `Signer is not the configured resolver`,
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -34,8 +34,10 @@ import {
|
||||
} from "@solana/kit";
|
||||
import {
|
||||
getAccountMetaFactory,
|
||||
getAddressFromResolvedInstructionAccount,
|
||||
type ResolvedInstructionAccount,
|
||||
} from "@solana/program-client-core";
|
||||
import { findVaultPda } from "../pdas";
|
||||
import { DESCRO_PROGRAM_ADDRESS } from "../programs";
|
||||
|
||||
export const CANCEL_DISCRIMINATOR: ReadonlyUint8Array = new Uint8Array([
|
||||
@@ -48,8 +50,10 @@ export function getCancelDiscriminatorBytes(): ReadonlyUint8Array {
|
||||
|
||||
export type CancelInstruction<
|
||||
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,
|
||||
TAccountVault extends string | AccountMeta<string> = string,
|
||||
TAccountSystemProgram extends string | AccountMeta<string> =
|
||||
"11111111111111111111111111111111",
|
||||
TRemainingAccounts extends readonly AccountMeta<string>[] = [],
|
||||
@@ -57,13 +61,19 @@ export type CancelInstruction<
|
||||
InstructionWithData<ReadonlyUint8Array> &
|
||||
InstructionWithAccounts<
|
||||
[
|
||||
TAccountSeller extends string
|
||||
? WritableSignerAccount<TAccountSeller> &
|
||||
AccountSignerMeta<TAccountSeller>
|
||||
: TAccountSeller,
|
||||
TAccountCanceller extends string
|
||||
? WritableSignerAccount<TAccountCanceller> &
|
||||
AccountSignerMeta<TAccountCanceller>
|
||||
: TAccountCanceller,
|
||||
TAccountBuyer extends string
|
||||
? WritableAccount<TAccountBuyer>
|
||||
: TAccountBuyer,
|
||||
TAccountEscrowAccount extends string
|
||||
? WritableAccount<TAccountEscrowAccount>
|
||||
: TAccountEscrowAccount,
|
||||
TAccountVault extends string
|
||||
? WritableAccount<TAccountVault>
|
||||
: TAccountVault,
|
||||
TAccountSystemProgram extends string
|
||||
? ReadonlyAccount<TAccountSystemProgram>
|
||||
: TAccountSystemProgram,
|
||||
@@ -98,32 +108,133 @@ export function getCancelInstructionDataCodec(): FixedSizeCodec<
|
||||
);
|
||||
}
|
||||
|
||||
export type CancelInput<
|
||||
TAccountSeller extends string = string,
|
||||
export type CancelAsyncInput<
|
||||
TAccountCanceller extends string = string,
|
||||
TAccountBuyer extends string = string,
|
||||
TAccountEscrowAccount extends string = string,
|
||||
TAccountVault extends string = string,
|
||||
TAccountSystemProgram extends string = string,
|
||||
> = {
|
||||
seller: TransactionSigner<TAccountSeller>;
|
||||
canceller: TransactionSigner<TAccountCanceller>;
|
||||
buyer: Address<TAccountBuyer>;
|
||||
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>;
|
||||
};
|
||||
|
||||
export function getCancelInstruction<
|
||||
TAccountSeller extends string,
|
||||
TAccountCanceller extends string,
|
||||
TAccountBuyer extends string,
|
||||
TAccountEscrowAccount extends string,
|
||||
TAccountVault extends string,
|
||||
TAccountSystemProgram extends string,
|
||||
TProgramAddress extends Address = typeof DESCRO_PROGRAM_ADDRESS,
|
||||
>(
|
||||
input: CancelInput<
|
||||
TAccountSeller,
|
||||
TAccountCanceller,
|
||||
TAccountBuyer,
|
||||
TAccountEscrowAccount,
|
||||
TAccountVault,
|
||||
TAccountSystemProgram
|
||||
>,
|
||||
config?: { programAddress?: TProgramAddress },
|
||||
): CancelInstruction<
|
||||
TProgramAddress,
|
||||
TAccountSeller,
|
||||
TAccountCanceller,
|
||||
TAccountBuyer,
|
||||
TAccountEscrowAccount,
|
||||
TAccountVault,
|
||||
TAccountSystemProgram
|
||||
> {
|
||||
// Program address.
|
||||
@@ -131,8 +242,10 @@ export function getCancelInstruction<
|
||||
|
||||
// Original accounts.
|
||||
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 },
|
||||
vault: { value: input.vault ?? null, isWritable: true },
|
||||
systemProgram: { value: input.systemProgram ?? null, isWritable: false },
|
||||
};
|
||||
const accounts = originalAccounts as Record<
|
||||
@@ -149,16 +262,20 @@ export function getCancelInstruction<
|
||||
const getAccountMeta = getAccountMetaFactory(programAddress, "programId");
|
||||
return Object.freeze({
|
||||
accounts: [
|
||||
getAccountMeta("seller", accounts.seller),
|
||||
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,
|
||||
TAccountSeller,
|
||||
TAccountCanceller,
|
||||
TAccountBuyer,
|
||||
TAccountEscrowAccount,
|
||||
TAccountVault,
|
||||
TAccountSystemProgram
|
||||
>);
|
||||
}
|
||||
@@ -169,9 +286,11 @@ export type ParsedCancelInstruction<
|
||||
> = {
|
||||
programAddress: Address<TProgram>;
|
||||
accounts: {
|
||||
seller: TAccountMetas[0];
|
||||
escrowAccount: TAccountMetas[1];
|
||||
systemProgram: TAccountMetas[2];
|
||||
canceller: TAccountMetas[0];
|
||||
buyer: TAccountMetas[1];
|
||||
escrowAccount: TAccountMetas[2];
|
||||
vault: TAccountMetas[3];
|
||||
systemProgram: TAccountMetas[4];
|
||||
};
|
||||
data: CancelInstructionData;
|
||||
};
|
||||
@@ -184,12 +303,12 @@ export function parseCancelInstruction<
|
||||
InstructionWithAccounts<TAccountMetas> &
|
||||
InstructionWithData<ReadonlyUint8Array>,
|
||||
): ParsedCancelInstruction<TProgram, TAccountMetas> {
|
||||
if (instruction.accounts.length < 3) {
|
||||
if (instruction.accounts.length < 5) {
|
||||
throw new SolanaError(
|
||||
SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS,
|
||||
{
|
||||
actualAccountMetas: instruction.accounts.length,
|
||||
expectedAccountMetas: 3,
|
||||
expectedAccountMetas: 5,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -202,8 +321,10 @@ export function parseCancelInstruction<
|
||||
return {
|
||||
programAddress: instruction.programAddress,
|
||||
accounts: {
|
||||
seller: getNextAccount(),
|
||||
canceller: getNextAccount(),
|
||||
buyer: getNextAccount(),
|
||||
escrowAccount: getNextAccount(),
|
||||
vault: getNextAccount(),
|
||||
systemProgram: getNextAccount(),
|
||||
},
|
||||
data: getCancelInstructionDataDecoder().decode(instruction.data),
|
||||
|
||||
@@ -63,8 +63,10 @@ export type CreateEscrowInstruction<
|
||||
TProgram extends string = typeof DESCRO_PROGRAM_ADDRESS,
|
||||
TAccountSeller extends string | AccountMeta<string> = string,
|
||||
TAccountBuyer extends string | AccountMeta<string> = string,
|
||||
TAccountResolver extends string | AccountMeta<string> = string,
|
||||
TAccountEscrowAccount extends string | AccountMeta<string> = string,
|
||||
TAccountVault extends string | AccountMeta<string> = string,
|
||||
TAccountResolverEntry extends string | AccountMeta<string> = string,
|
||||
TAccountSystemProgram extends string | AccountMeta<string> =
|
||||
"11111111111111111111111111111111",
|
||||
TRemainingAccounts extends readonly AccountMeta<string>[] = [],
|
||||
@@ -79,12 +81,18 @@ export type CreateEscrowInstruction<
|
||||
TAccountBuyer extends string
|
||||
? ReadonlyAccount<TAccountBuyer>
|
||||
: TAccountBuyer,
|
||||
TAccountResolver extends string
|
||||
? ReadonlyAccount<TAccountResolver>
|
||||
: TAccountResolver,
|
||||
TAccountEscrowAccount extends string
|
||||
? WritableAccount<TAccountEscrowAccount>
|
||||
: TAccountEscrowAccount,
|
||||
TAccountVault extends string
|
||||
? WritableAccount<TAccountVault>
|
||||
: TAccountVault,
|
||||
TAccountResolverEntry extends string
|
||||
? ReadonlyAccount<TAccountResolverEntry>
|
||||
: TAccountResolverEntry,
|
||||
TAccountSystemProgram extends string
|
||||
? ReadonlyAccount<TAccountSystemProgram>
|
||||
: TAccountSystemProgram,
|
||||
@@ -139,14 +147,18 @@ export function getCreateEscrowInstructionDataCodec(): Codec<
|
||||
export type CreateEscrowAsyncInput<
|
||||
TAccountSeller extends string = string,
|
||||
TAccountBuyer extends string = string,
|
||||
TAccountResolver extends string = string,
|
||||
TAccountEscrowAccount extends string = string,
|
||||
TAccountVault extends string = string,
|
||||
TAccountResolverEntry extends string = string,
|
||||
TAccountSystemProgram extends string = string,
|
||||
> = {
|
||||
seller: TransactionSigner<TAccountSeller>;
|
||||
buyer: Address<TAccountBuyer>;
|
||||
resolver: Address<TAccountResolver>;
|
||||
escrowAccount?: Address<TAccountEscrowAccount>;
|
||||
vault?: Address<TAccountVault>;
|
||||
resolverEntry: Address<TAccountResolverEntry>;
|
||||
systemProgram?: Address<TAccountSystemProgram>;
|
||||
amount: CreateEscrowInstructionDataArgs["amount"];
|
||||
disputeResolver: CreateEscrowInstructionDataArgs["disputeResolver"];
|
||||
@@ -156,16 +168,20 @@ export type CreateEscrowAsyncInput<
|
||||
export async function getCreateEscrowInstructionAsync<
|
||||
TAccountSeller extends string,
|
||||
TAccountBuyer extends string,
|
||||
TAccountResolver extends string,
|
||||
TAccountEscrowAccount extends string,
|
||||
TAccountVault extends string,
|
||||
TAccountResolverEntry extends string,
|
||||
TAccountSystemProgram extends string,
|
||||
TProgramAddress extends Address = typeof DESCRO_PROGRAM_ADDRESS,
|
||||
>(
|
||||
input: CreateEscrowAsyncInput<
|
||||
TAccountSeller,
|
||||
TAccountBuyer,
|
||||
TAccountResolver,
|
||||
TAccountEscrowAccount,
|
||||
TAccountVault,
|
||||
TAccountResolverEntry,
|
||||
TAccountSystemProgram
|
||||
>,
|
||||
config?: { programAddress?: TProgramAddress },
|
||||
@@ -174,8 +190,10 @@ export async function getCreateEscrowInstructionAsync<
|
||||
TProgramAddress,
|
||||
TAccountSeller,
|
||||
TAccountBuyer,
|
||||
TAccountResolver,
|
||||
TAccountEscrowAccount,
|
||||
TAccountVault,
|
||||
TAccountResolverEntry,
|
||||
TAccountSystemProgram
|
||||
>
|
||||
> {
|
||||
@@ -186,8 +204,10 @@ export async function getCreateEscrowInstructionAsync<
|
||||
const originalAccounts = {
|
||||
seller: { value: input.seller ?? null, isWritable: true },
|
||||
buyer: { value: input.buyer ?? null, isWritable: false },
|
||||
resolver: { value: input.resolver ?? null, isWritable: false },
|
||||
escrowAccount: { value: input.escrowAccount ?? null, isWritable: true },
|
||||
vault: { value: input.vault ?? null, isWritable: true },
|
||||
resolverEntry: { value: input.resolverEntry ?? null, isWritable: false },
|
||||
systemProgram: { value: input.systemProgram ?? null, isWritable: false },
|
||||
};
|
||||
const accounts = originalAccounts as Record<
|
||||
@@ -226,8 +246,10 @@ export async function getCreateEscrowInstructionAsync<
|
||||
accounts: [
|
||||
getAccountMeta("seller", accounts.seller),
|
||||
getAccountMeta("buyer", accounts.buyer),
|
||||
getAccountMeta("resolver", accounts.resolver),
|
||||
getAccountMeta("escrowAccount", accounts.escrowAccount),
|
||||
getAccountMeta("vault", accounts.vault),
|
||||
getAccountMeta("resolverEntry", accounts.resolverEntry),
|
||||
getAccountMeta("systemProgram", accounts.systemProgram),
|
||||
],
|
||||
data: getCreateEscrowInstructionDataEncoder().encode(
|
||||
@@ -238,8 +260,10 @@ export async function getCreateEscrowInstructionAsync<
|
||||
TProgramAddress,
|
||||
TAccountSeller,
|
||||
TAccountBuyer,
|
||||
TAccountResolver,
|
||||
TAccountEscrowAccount,
|
||||
TAccountVault,
|
||||
TAccountResolverEntry,
|
||||
TAccountSystemProgram
|
||||
>);
|
||||
}
|
||||
@@ -247,14 +271,18 @@ export async function getCreateEscrowInstructionAsync<
|
||||
export type CreateEscrowInput<
|
||||
TAccountSeller extends string = string,
|
||||
TAccountBuyer extends string = string,
|
||||
TAccountResolver extends string = string,
|
||||
TAccountEscrowAccount extends string = string,
|
||||
TAccountVault extends string = string,
|
||||
TAccountResolverEntry extends string = string,
|
||||
TAccountSystemProgram extends string = string,
|
||||
> = {
|
||||
seller: TransactionSigner<TAccountSeller>;
|
||||
buyer: Address<TAccountBuyer>;
|
||||
resolver: Address<TAccountResolver>;
|
||||
escrowAccount: Address<TAccountEscrowAccount>;
|
||||
vault: Address<TAccountVault>;
|
||||
resolverEntry: Address<TAccountResolverEntry>;
|
||||
systemProgram?: Address<TAccountSystemProgram>;
|
||||
amount: CreateEscrowInstructionDataArgs["amount"];
|
||||
disputeResolver: CreateEscrowInstructionDataArgs["disputeResolver"];
|
||||
@@ -264,16 +292,20 @@ export type CreateEscrowInput<
|
||||
export function getCreateEscrowInstruction<
|
||||
TAccountSeller extends string,
|
||||
TAccountBuyer extends string,
|
||||
TAccountResolver extends string,
|
||||
TAccountEscrowAccount extends string,
|
||||
TAccountVault extends string,
|
||||
TAccountResolverEntry extends string,
|
||||
TAccountSystemProgram extends string,
|
||||
TProgramAddress extends Address = typeof DESCRO_PROGRAM_ADDRESS,
|
||||
>(
|
||||
input: CreateEscrowInput<
|
||||
TAccountSeller,
|
||||
TAccountBuyer,
|
||||
TAccountResolver,
|
||||
TAccountEscrowAccount,
|
||||
TAccountVault,
|
||||
TAccountResolverEntry,
|
||||
TAccountSystemProgram
|
||||
>,
|
||||
config?: { programAddress?: TProgramAddress },
|
||||
@@ -281,8 +313,10 @@ export function getCreateEscrowInstruction<
|
||||
TProgramAddress,
|
||||
TAccountSeller,
|
||||
TAccountBuyer,
|
||||
TAccountResolver,
|
||||
TAccountEscrowAccount,
|
||||
TAccountVault,
|
||||
TAccountResolverEntry,
|
||||
TAccountSystemProgram
|
||||
> {
|
||||
// Program address.
|
||||
@@ -292,8 +326,10 @@ export function getCreateEscrowInstruction<
|
||||
const originalAccounts = {
|
||||
seller: { value: input.seller ?? null, isWritable: true },
|
||||
buyer: { value: input.buyer ?? null, isWritable: false },
|
||||
resolver: { value: input.resolver ?? null, isWritable: false },
|
||||
escrowAccount: { value: input.escrowAccount ?? null, isWritable: true },
|
||||
vault: { value: input.vault ?? null, isWritable: true },
|
||||
resolverEntry: { value: input.resolverEntry ?? null, isWritable: false },
|
||||
systemProgram: { value: input.systemProgram ?? null, isWritable: false },
|
||||
};
|
||||
const accounts = originalAccounts as Record<
|
||||
@@ -315,8 +351,10 @@ export function getCreateEscrowInstruction<
|
||||
accounts: [
|
||||
getAccountMeta("seller", accounts.seller),
|
||||
getAccountMeta("buyer", accounts.buyer),
|
||||
getAccountMeta("resolver", accounts.resolver),
|
||||
getAccountMeta("escrowAccount", accounts.escrowAccount),
|
||||
getAccountMeta("vault", accounts.vault),
|
||||
getAccountMeta("resolverEntry", accounts.resolverEntry),
|
||||
getAccountMeta("systemProgram", accounts.systemProgram),
|
||||
],
|
||||
data: getCreateEscrowInstructionDataEncoder().encode(
|
||||
@@ -327,8 +365,10 @@ export function getCreateEscrowInstruction<
|
||||
TProgramAddress,
|
||||
TAccountSeller,
|
||||
TAccountBuyer,
|
||||
TAccountResolver,
|
||||
TAccountEscrowAccount,
|
||||
TAccountVault,
|
||||
TAccountResolverEntry,
|
||||
TAccountSystemProgram
|
||||
>);
|
||||
}
|
||||
@@ -341,9 +381,11 @@ export type ParsedCreateEscrowInstruction<
|
||||
accounts: {
|
||||
seller: TAccountMetas[0];
|
||||
buyer: TAccountMetas[1];
|
||||
escrowAccount: TAccountMetas[2];
|
||||
vault: TAccountMetas[3];
|
||||
systemProgram: TAccountMetas[4];
|
||||
resolver: TAccountMetas[2];
|
||||
escrowAccount: TAccountMetas[3];
|
||||
vault: TAccountMetas[4];
|
||||
resolverEntry: TAccountMetas[5];
|
||||
systemProgram: TAccountMetas[6];
|
||||
};
|
||||
data: CreateEscrowInstructionData;
|
||||
};
|
||||
@@ -356,12 +398,12 @@ export function parseCreateEscrowInstruction<
|
||||
InstructionWithAccounts<TAccountMetas> &
|
||||
InstructionWithData<ReadonlyUint8Array>,
|
||||
): ParsedCreateEscrowInstruction<TProgram, TAccountMetas> {
|
||||
if (instruction.accounts.length < 5) {
|
||||
if (instruction.accounts.length < 7) {
|
||||
throw new SolanaError(
|
||||
SOLANA_ERROR__PROGRAM_CLIENTS__INSUFFICIENT_ACCOUNT_METAS,
|
||||
{
|
||||
actualAccountMetas: instruction.accounts.length,
|
||||
expectedAccountMetas: 5,
|
||||
expectedAccountMetas: 7,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -376,8 +418,10 @@ export function parseCreateEscrowInstruction<
|
||||
accounts: {
|
||||
seller: getNextAccount(),
|
||||
buyer: getNextAccount(),
|
||||
resolver: getNextAccount(),
|
||||
escrowAccount: getNextAccount(),
|
||||
vault: getNextAccount(),
|
||||
resolverEntry: getNextAccount(),
|
||||
systemProgram: getNextAccount(),
|
||||
},
|
||||
data: getCreateEscrowInstructionDataDecoder().decode(instruction.data),
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -6,9 +6,12 @@
|
||||
* @see https://github.com/codama-idl/codama
|
||||
*/
|
||||
|
||||
export * from "./buyerCreateEscrow";
|
||||
export * from "./cancel";
|
||||
export * from "./complete";
|
||||
export * from "./createEscrow";
|
||||
export * from "./deposit";
|
||||
export * from "./dispute";
|
||||
export * from "./emergencyResolve";
|
||||
export * from "./resolve";
|
||||
export * from "./sellerConfirm";
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -38,30 +38,42 @@ import {
|
||||
type EscrowAccountArgs,
|
||||
} from "../accounts";
|
||||
import {
|
||||
getCancelInstruction,
|
||||
getBuyerCreateEscrowInstructionAsync,
|
||||
getCancelInstructionAsync,
|
||||
getCompleteInstructionAsync,
|
||||
getCreateEscrowInstructionAsync,
|
||||
getDepositInstructionAsync,
|
||||
getDisputeInstruction,
|
||||
getEmergencyResolveInstructionAsync,
|
||||
getResolveInstructionAsync,
|
||||
getSellerConfirmInstruction,
|
||||
parseBuyerCreateEscrowInstruction,
|
||||
parseCancelInstruction,
|
||||
parseCompleteInstruction,
|
||||
parseCreateEscrowInstruction,
|
||||
parseDepositInstruction,
|
||||
parseDisputeInstruction,
|
||||
parseEmergencyResolveInstruction,
|
||||
parseResolveInstruction,
|
||||
type CancelInput,
|
||||
parseSellerConfirmInstruction,
|
||||
type BuyerCreateEscrowAsyncInput,
|
||||
type CancelAsyncInput,
|
||||
type CompleteAsyncInput,
|
||||
type CreateEscrowAsyncInput,
|
||||
type DepositAsyncInput,
|
||||
type DisputeInput,
|
||||
type EmergencyResolveAsyncInput,
|
||||
type ParsedBuyerCreateEscrowInstruction,
|
||||
type ParsedCancelInstruction,
|
||||
type ParsedCompleteInstruction,
|
||||
type ParsedCreateEscrowInstruction,
|
||||
type ParsedDepositInstruction,
|
||||
type ParsedDisputeInstruction,
|
||||
type ParsedEmergencyResolveInstruction,
|
||||
type ParsedResolveInstruction,
|
||||
type ParsedSellerConfirmInstruction,
|
||||
type ResolveAsyncInput,
|
||||
type SellerConfirmInput,
|
||||
} from "../instructions";
|
||||
import {
|
||||
findEscrowAccountPda,
|
||||
@@ -98,18 +110,32 @@ export function identifyDescroAccount(
|
||||
}
|
||||
|
||||
export enum DescroInstruction {
|
||||
BuyerCreateEscrow,
|
||||
Cancel,
|
||||
Complete,
|
||||
CreateEscrow,
|
||||
Deposit,
|
||||
Dispute,
|
||||
EmergencyResolve,
|
||||
Resolve,
|
||||
SellerConfirm,
|
||||
}
|
||||
|
||||
export function identifyDescroInstruction(
|
||||
instruction: { data: ReadonlyUint8Array } | ReadonlyUint8Array,
|
||||
): DescroInstruction {
|
||||
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 (
|
||||
containsBytes(
|
||||
data,
|
||||
@@ -165,6 +191,17 @@ export function identifyDescroInstruction(
|
||||
) {
|
||||
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 (
|
||||
containsBytes(
|
||||
data,
|
||||
@@ -176,6 +213,17 @@ export function identifyDescroInstruction(
|
||||
) {
|
||||
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(
|
||||
SOLANA_ERROR__PROGRAM_CLIENTS__FAILED_TO_IDENTIFY_INSTRUCTION,
|
||||
{ instructionData: data, programName: "descro" },
|
||||
@@ -185,6 +233,9 @@ export function identifyDescroInstruction(
|
||||
export type ParsedDescroInstruction<
|
||||
TProgram extends string = "DjVR4EuYV6USMJFfsGZwhZ3y8rtWsmG8EvDY96GTqqi3",
|
||||
> =
|
||||
| ({
|
||||
instructionType: DescroInstruction.BuyerCreateEscrow;
|
||||
} & ParsedBuyerCreateEscrowInstruction<TProgram>)
|
||||
| ({
|
||||
instructionType: DescroInstruction.Cancel;
|
||||
} & ParsedCancelInstruction<TProgram>)
|
||||
@@ -200,15 +251,28 @@ export type ParsedDescroInstruction<
|
||||
| ({
|
||||
instructionType: DescroInstruction.Dispute;
|
||||
} & ParsedDisputeInstruction<TProgram>)
|
||||
| ({
|
||||
instructionType: DescroInstruction.EmergencyResolve;
|
||||
} & ParsedEmergencyResolveInstruction<TProgram>)
|
||||
| ({
|
||||
instructionType: DescroInstruction.Resolve;
|
||||
} & ParsedResolveInstruction<TProgram>);
|
||||
} & ParsedResolveInstruction<TProgram>)
|
||||
| ({
|
||||
instructionType: DescroInstruction.SellerConfirm;
|
||||
} & ParsedSellerConfirmInstruction<TProgram>);
|
||||
|
||||
export function parseDescroInstruction<TProgram extends string>(
|
||||
instruction: Instruction<TProgram> & InstructionWithData<ReadonlyUint8Array>,
|
||||
): ParsedDescroInstruction<TProgram> {
|
||||
const instructionType = identifyDescroInstruction(instruction);
|
||||
switch (instructionType) {
|
||||
case DescroInstruction.BuyerCreateEscrow: {
|
||||
assertIsInstructionWithAccounts(instruction);
|
||||
return {
|
||||
instructionType: DescroInstruction.BuyerCreateEscrow,
|
||||
...parseBuyerCreateEscrowInstruction(instruction),
|
||||
};
|
||||
}
|
||||
case DescroInstruction.Cancel: {
|
||||
assertIsInstructionWithAccounts(instruction);
|
||||
return {
|
||||
@@ -244,6 +308,13 @@ export function parseDescroInstruction<TProgram extends string>(
|
||||
...parseDisputeInstruction(instruction),
|
||||
};
|
||||
}
|
||||
case DescroInstruction.EmergencyResolve: {
|
||||
assertIsInstructionWithAccounts(instruction);
|
||||
return {
|
||||
instructionType: DescroInstruction.EmergencyResolve,
|
||||
...parseEmergencyResolveInstruction(instruction),
|
||||
};
|
||||
}
|
||||
case DescroInstruction.Resolve: {
|
||||
assertIsInstructionWithAccounts(instruction);
|
||||
return {
|
||||
@@ -251,6 +322,13 @@ export function parseDescroInstruction<TProgram extends string>(
|
||||
...parseResolveInstruction(instruction),
|
||||
};
|
||||
}
|
||||
case DescroInstruction.SellerConfirm: {
|
||||
assertIsInstructionWithAccounts(instruction);
|
||||
return {
|
||||
instructionType: DescroInstruction.SellerConfirm,
|
||||
...parseSellerConfirmInstruction(instruction),
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new SolanaError(
|
||||
SOLANA_ERROR__PROGRAM_CLIENTS__UNRECOGNIZED_INSTRUCTION_TYPE,
|
||||
@@ -271,9 +349,13 @@ export type DescroPluginAccounts = {
|
||||
};
|
||||
|
||||
export type DescroPluginInstructions = {
|
||||
buyerCreateEscrow: (
|
||||
input: BuyerCreateEscrowAsyncInput,
|
||||
) => ReturnType<typeof getBuyerCreateEscrowInstructionAsync> &
|
||||
SelfPlanAndSendFunctions;
|
||||
cancel: (
|
||||
input: CancelInput,
|
||||
) => ReturnType<typeof getCancelInstruction> & SelfPlanAndSendFunctions;
|
||||
input: CancelAsyncInput,
|
||||
) => ReturnType<typeof getCancelInstructionAsync> & SelfPlanAndSendFunctions;
|
||||
complete: (
|
||||
input: CompleteAsyncInput,
|
||||
) => ReturnType<typeof getCompleteInstructionAsync> &
|
||||
@@ -288,14 +370,22 @@ export type DescroPluginInstructions = {
|
||||
dispute: (
|
||||
input: DisputeInput,
|
||||
) => ReturnType<typeof getDisputeInstruction> & SelfPlanAndSendFunctions;
|
||||
emergencyResolve: (
|
||||
input: EmergencyResolveAsyncInput,
|
||||
) => ReturnType<typeof getEmergencyResolveInstructionAsync> &
|
||||
SelfPlanAndSendFunctions;
|
||||
resolve: (
|
||||
input: ResolveAsyncInput,
|
||||
) => ReturnType<typeof getResolveInstructionAsync> & SelfPlanAndSendFunctions;
|
||||
sellerConfirm: (
|
||||
input: SellerConfirmInput,
|
||||
) => ReturnType<typeof getSellerConfirmInstruction> &
|
||||
SelfPlanAndSendFunctions;
|
||||
};
|
||||
|
||||
export type DescroPluginPdas = {
|
||||
vault: typeof findVaultPda;
|
||||
escrowAccount: typeof findEscrowAccountPda;
|
||||
vault: typeof findVaultPda;
|
||||
escrowAuthority: typeof findEscrowAuthorityPda;
|
||||
};
|
||||
|
||||
@@ -315,8 +405,16 @@ export function descroProgram() {
|
||||
escrowAccount: addSelfFetchFunctions(client, getEscrowAccountCodec()),
|
||||
},
|
||||
instructions: {
|
||||
buyerCreateEscrow: (input) =>
|
||||
addSelfPlanAndSendFunctions(
|
||||
client,
|
||||
getBuyerCreateEscrowInstructionAsync(input),
|
||||
),
|
||||
cancel: (input) =>
|
||||
addSelfPlanAndSendFunctions(client, getCancelInstruction(input)),
|
||||
addSelfPlanAndSendFunctions(
|
||||
client,
|
||||
getCancelInstructionAsync(input),
|
||||
),
|
||||
complete: (input) =>
|
||||
addSelfPlanAndSendFunctions(
|
||||
client,
|
||||
@@ -334,15 +432,25 @@ export function descroProgram() {
|
||||
),
|
||||
dispute: (input) =>
|
||||
addSelfPlanAndSendFunctions(client, getDisputeInstruction(input)),
|
||||
emergencyResolve: (input) =>
|
||||
addSelfPlanAndSendFunctions(
|
||||
client,
|
||||
getEmergencyResolveInstructionAsync(input),
|
||||
),
|
||||
resolve: (input) =>
|
||||
addSelfPlanAndSendFunctions(
|
||||
client,
|
||||
getResolveInstructionAsync(input),
|
||||
),
|
||||
sellerConfirm: (input) =>
|
||||
addSelfPlanAndSendFunctions(
|
||||
client,
|
||||
getSellerConfirmInstruction(input),
|
||||
),
|
||||
},
|
||||
pdas: {
|
||||
vault: findVaultPda,
|
||||
escrowAccount: findEscrowAccountPda,
|
||||
vault: findVaultPda,
|
||||
escrowAuthority: findEscrowAuthorityPda,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
|
||||
export enum EscrowState {
|
||||
AwaitingDeposit,
|
||||
AwaitingSellerConfirm,
|
||||
Active,
|
||||
Disputed,
|
||||
Complete,
|
||||
|
||||
@@ -47,8 +47,12 @@ import {
|
||||
type ReadonlyUint8Array,
|
||||
} from "@solana/kit";
|
||||
import {
|
||||
getAcceptancePolicyDecoder,
|
||||
getAcceptancePolicyEncoder,
|
||||
getResolverTypeDecoder,
|
||||
getResolverTypeEncoder,
|
||||
type AcceptancePolicy,
|
||||
type AcceptancePolicyArgs,
|
||||
type ResolverType,
|
||||
type ResolverTypeArgs,
|
||||
} from "../types";
|
||||
@@ -67,6 +71,7 @@ export type ResolverEntry = {
|
||||
discriminator: ReadonlyUint8Array;
|
||||
authority: Address;
|
||||
resolverType: ResolverType;
|
||||
acceptancePolicy: AcceptancePolicy;
|
||||
name: string;
|
||||
description: string;
|
||||
feeBps: number;
|
||||
@@ -81,6 +86,7 @@ export type ResolverEntry = {
|
||||
export type ResolverEntryArgs = {
|
||||
authority: Address;
|
||||
resolverType: ResolverTypeArgs;
|
||||
acceptancePolicy: AcceptancePolicyArgs;
|
||||
name: string;
|
||||
description: string;
|
||||
feeBps: number;
|
||||
@@ -99,6 +105,7 @@ export function getResolverEntryEncoder(): Encoder<ResolverEntryArgs> {
|
||||
["discriminator", fixEncoderSize(getBytesEncoder(), 8)],
|
||||
["authority", getAddressEncoder()],
|
||||
["resolverType", getResolverTypeEncoder()],
|
||||
["acceptancePolicy", getAcceptancePolicyEncoder()],
|
||||
["name", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||
["description", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||
["feeBps", getU16Encoder()],
|
||||
@@ -119,6 +126,7 @@ export function getResolverEntryDecoder(): Decoder<ResolverEntry> {
|
||||
["discriminator", fixDecoderSize(getBytesDecoder(), 8)],
|
||||
["authority", getAddressDecoder()],
|
||||
["resolverType", getResolverTypeDecoder()],
|
||||
["acceptancePolicy", getAcceptancePolicyDecoder()],
|
||||
["name", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||
["description", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||
["feeBps", getU16Decoder()],
|
||||
|
||||
@@ -50,8 +50,12 @@ import {
|
||||
import { findResolverEntryPda } from "../pdas";
|
||||
import { DESCRO_EXT_RESOLVERS_PROGRAM_ADDRESS } from "../programs";
|
||||
import {
|
||||
getAcceptancePolicyDecoder,
|
||||
getAcceptancePolicyEncoder,
|
||||
getResolverTypeDecoder,
|
||||
getResolverTypeEncoder,
|
||||
type AcceptancePolicy,
|
||||
type AcceptancePolicyArgs,
|
||||
type ResolverType,
|
||||
type ResolverTypeArgs,
|
||||
} from "../types";
|
||||
@@ -93,6 +97,7 @@ export type RegisterResolverInstruction<
|
||||
export type RegisterResolverInstructionData = {
|
||||
discriminator: ReadonlyUint8Array;
|
||||
resolverType: ResolverType;
|
||||
acceptancePolicy: AcceptancePolicy;
|
||||
name: string;
|
||||
description: string;
|
||||
feeBps: number;
|
||||
@@ -102,6 +107,7 @@ export type RegisterResolverInstructionData = {
|
||||
|
||||
export type RegisterResolverInstructionDataArgs = {
|
||||
resolverType: ResolverTypeArgs;
|
||||
acceptancePolicy: AcceptancePolicyArgs;
|
||||
name: string;
|
||||
description: string;
|
||||
feeBps: number;
|
||||
@@ -114,6 +120,7 @@ export function getRegisterResolverInstructionDataEncoder(): Encoder<RegisterRes
|
||||
getStructEncoder([
|
||||
["discriminator", fixEncoderSize(getBytesEncoder(), 8)],
|
||||
["resolverType", getResolverTypeEncoder()],
|
||||
["acceptancePolicy", getAcceptancePolicyEncoder()],
|
||||
["name", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||
["description", addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())],
|
||||
["feeBps", getU16Encoder()],
|
||||
@@ -128,6 +135,7 @@ export function getRegisterResolverInstructionDataDecoder(): Decoder<RegisterRes
|
||||
return getStructDecoder([
|
||||
["discriminator", fixDecoderSize(getBytesDecoder(), 8)],
|
||||
["resolverType", getResolverTypeDecoder()],
|
||||
["acceptancePolicy", getAcceptancePolicyDecoder()],
|
||||
["name", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||
["description", addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
|
||||
["feeBps", getU16Decoder()],
|
||||
@@ -155,6 +163,7 @@ export type RegisterResolverAsyncInput<
|
||||
resolverEntry?: Address<TAccountResolverEntry>;
|
||||
systemProgram?: Address<TAccountSystemProgram>;
|
||||
resolverType: RegisterResolverInstructionDataArgs["resolverType"];
|
||||
acceptancePolicy: RegisterResolverInstructionDataArgs["acceptancePolicy"];
|
||||
name: RegisterResolverInstructionDataArgs["name"];
|
||||
description: RegisterResolverInstructionDataArgs["description"];
|
||||
feeBps: RegisterResolverInstructionDataArgs["feeBps"];
|
||||
@@ -242,6 +251,7 @@ export type RegisterResolverInput<
|
||||
resolverEntry: Address<TAccountResolverEntry>;
|
||||
systemProgram?: Address<TAccountSystemProgram>;
|
||||
resolverType: RegisterResolverInstructionDataArgs["resolverType"];
|
||||
acceptancePolicy: RegisterResolverInstructionDataArgs["acceptancePolicy"];
|
||||
name: RegisterResolverInstructionDataArgs["name"];
|
||||
description: RegisterResolverInstructionDataArgs["description"];
|
||||
feeBps: RegisterResolverInstructionDataArgs["feeBps"];
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
@@ -6,5 +6,6 @@
|
||||
* @see https://github.com/codama-idl/codama
|
||||
*/
|
||||
|
||||
export * from "./acceptancePolicy";
|
||||
export * from "./resolverType";
|
||||
export * from "./ruling";
|
||||
|
||||
@@ -7,6 +7,98 @@
|
||||
"description": "Created with Anchor"
|
||||
},
|
||||
"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",
|
||||
"discriminator": [
|
||||
@@ -21,10 +113,14 @@
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "seller",
|
||||
"name": "canceller",
|
||||
"writable": true,
|
||||
"signer": true
|
||||
},
|
||||
{
|
||||
"name": "buyer",
|
||||
"writable": true
|
||||
},
|
||||
{
|
||||
"name": "escrow_account",
|
||||
"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",
|
||||
"address": "11111111111111111111111111111111"
|
||||
@@ -161,6 +279,9 @@
|
||||
{
|
||||
"name": "buyer"
|
||||
},
|
||||
{
|
||||
"name": "resolver"
|
||||
},
|
||||
{
|
||||
"name": "escrow_account",
|
||||
"writable": true,
|
||||
@@ -210,6 +331,9 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "resolver_entry"
|
||||
},
|
||||
{
|
||||
"name": "system_program",
|
||||
"address": "11111111111111111111111111111111"
|
||||
@@ -357,6 +481,99 @@
|
||||
],
|
||||
"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",
|
||||
"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": [
|
||||
@@ -584,6 +855,26 @@
|
||||
"code": 6004,
|
||||
"name": "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": [
|
||||
@@ -629,6 +920,12 @@
|
||||
{
|
||||
"name": "escrow_id",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "dispute_raised_at",
|
||||
"type": {
|
||||
"option": "i64"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -641,6 +938,9 @@
|
||||
{
|
||||
"name": "AwaitingDeposit"
|
||||
},
|
||||
{
|
||||
"name": "AwaitingSellerConfirm"
|
||||
},
|
||||
{
|
||||
"name": "Active"
|
||||
},
|
||||
|
||||
@@ -64,6 +64,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "acceptance_policy",
|
||||
"type": {
|
||||
"defined": {
|
||||
"name": "AcceptancePolicy"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
@@ -285,6 +293,23 @@
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
{
|
||||
"name": "AcceptancePolicy",
|
||||
"type": {
|
||||
"kind": "enum",
|
||||
"variants": [
|
||||
{
|
||||
"name": "Open"
|
||||
},
|
||||
{
|
||||
"name": "SignatureGated"
|
||||
},
|
||||
{
|
||||
"name": "ProgramGated"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ResolverEntry",
|
||||
"type": {
|
||||
@@ -302,6 +327,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "acceptance_policy",
|
||||
"type": {
|
||||
"defined": {
|
||||
"name": "AcceptancePolicy"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
|
||||
@@ -24,8 +24,20 @@ export {
|
||||
} from "./generated/descro/src/generated/instructions/complete";
|
||||
export {
|
||||
getCancelInstruction,
|
||||
getCancelInstructionAsync,
|
||||
type CancelInput,
|
||||
type CancelAsyncInput,
|
||||
} 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 {
|
||||
getDisputeInstruction,
|
||||
type DisputeInput,
|
||||
@@ -43,4 +55,5 @@ export {
|
||||
type RegisterResolverInput,
|
||||
} from "./generated/descro_ext_resolvers/src/generated/instructions/registerResolver";
|
||||
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";
|
||||
@@ -32,6 +32,9 @@ export type ResolverEntryWithPda = { pda: Address; account: ResolverEntry };
|
||||
export function isAwaitingDeposit(state: EscrowState): boolean {
|
||||
return state === EscrowState.AwaitingDeposit;
|
||||
}
|
||||
export function isAwaitingSellerConfirm(state: EscrowState): boolean {
|
||||
return state === EscrowState.AwaitingSellerConfirm;
|
||||
}
|
||||
export function isActive(state: EscrowState): boolean {
|
||||
return state === EscrowState.Active;
|
||||
}
|
||||
@@ -48,6 +51,7 @@ export function isCancelled(state: EscrowState): boolean {
|
||||
export function escrowStateLabel(state: EscrowState): string {
|
||||
switch (state) {
|
||||
case EscrowState.AwaitingDeposit: return "AwaitingDeposit";
|
||||
case EscrowState.AwaitingSellerConfirm: return "AwaitingSellerConfirm";
|
||||
case EscrowState.Active: return "Active";
|
||||
case EscrowState.Disputed: return "Disputed";
|
||||
case EscrowState.Complete: return "Complete";
|
||||
|
||||
Reference in New Issue
Block a user