Skip to content

Commit c9eda72

Browse files
committed
Release v1.4.0
1 parent a2a2cf2 commit c9eda72

8 files changed

Lines changed: 313 additions & 6 deletions

File tree

.agents/rules.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,20 @@ CLI (src/cli.ts) SDK (src/sdk.ts)
5757

5858
Each domain has both a CLI command file (`src/commands/<domain>.ts`) and an SDK class (`src/sdk.ts`):
5959

60+
- **accounts** - Account profile lookup
61+
- **auth** - API key request and management
62+
- **chains** - Chain information and supported networks
6063
- **collections** - Collection metadata, stats, traits
61-
- **nfts** - NFT lookup, listing by collection/contract/account, metadata refresh
64+
- **drops** - NFT drop details, listing, and minting
65+
- **events** - Marketplace events (sales, transfers, mints, etc.)
66+
- **health** - API health check
6267
- **listings** - Active listings (all, best, best-for-nft)
68+
- **nfts** - NFT lookup, listing by collection/contract/account, metadata refresh
6369
- **offers** - Offers (all, collection, best-for-nft, trait offers)
64-
- **events** - Marketplace events (sales, transfers, mints, etc.)
65-
- **accounts** - Account profile lookup
66-
- **tokens** - Fungible token trending/top/details
70+
- **search** - Search collections, NFTs, and accounts
6771
- **swaps** - Token swap quotes
72+
- **token-groups** - Token group details and listings
73+
- **tokens** - Fungible token trending/top/details
6874

6975
## Conventions
7076

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# @opensea/cli
22

3+
## 1.4.0
4+
5+
### Minor Changes
6+
7+
- fc44d9f: feat: add cross-chain fulfillment support
8+
9+
Add support for the new `POST /api/v2/listings/cross_chain_fulfillment_data` endpoint across SDK, CLI, and skill packages.
10+
11+
**SDK**: New `getCrossChainFulfillmentData()` method on both the API client and the base SDK class. Accepts listings, fulfiller, payment token (chain + address), and optional recipient. Returns ordered transactions to sign and submit.
12+
13+
**CLI**: New `listings cross-chain-fulfill` subcommand with `--hashes`, `--listing-chain`, `--protocol-address`, `--fulfiller`, `--payment-chain`, `--payment-token`, and optional `--recipient` flags. Supports sweeping multiple listings via comma-separated hashes.
14+
15+
**Skill**: New `opensea-cross-chain-fulfill.sh` script and updated SKILL.md with cross-chain buying workflow documentation.
16+
317
## 1.3.0
418

519
### Minor Changes

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opensea/cli",
3-
"version": "1.3.0",
3+
"version": "1.4.0",
44
"type": "module",
55
"description": "OpenSea CLI - Query the OpenSea API from the command line or programmatically",
66
"main": "dist/index.js",

src/commands/listings.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import type { OpenSeaClient } from "../client.js"
33
import type { OutputFormat } from "../output.js"
44
import { formatOutput } from "../output.js"
55
import { addTraitsOption, parseIntOption, parseTraitsOption } from "../parse.js"
6-
import type { Listing } from "../types/index.js"
6+
import type {
7+
CrossChainFulfillmentDataResponse,
8+
Listing,
9+
} from "../types/index.js"
710

811
export function listingsCommand(
912
getClient: () => OpenSeaClient,
@@ -69,5 +72,69 @@ export function listingsCommand(
6972
console.log(formatOutput(result, getFormat()))
7073
})
7174

75+
cmd
76+
.command("cross-chain-fulfill")
77+
.description(
78+
"Get cross-chain fulfillment data for one or more listings. " +
79+
"Supports same-chain, cross-token, and cross-chain purchases.",
80+
)
81+
.requiredOption(
82+
"--hashes <hashes>",
83+
"Comma-separated order hashes to fulfill",
84+
)
85+
.requiredOption(
86+
"--listing-chain <chain>",
87+
"Chain slug where the listings live (must be EVM)",
88+
)
89+
.requiredOption(
90+
"--protocol-address <address>",
91+
"Seaport contract address for the listings",
92+
)
93+
.requiredOption("--fulfiller <address>", "Buyer wallet address")
94+
.requiredOption(
95+
"--payment-chain <chain>",
96+
"Chain slug of the payment token (EVM or SVM)",
97+
)
98+
.requiredOption(
99+
"--payment-token <address>",
100+
"Payment token contract address (0x0...0 for native)",
101+
)
102+
.option("--recipient <address>", "Different recipient address for NFTs")
103+
.action(
104+
async (options: {
105+
hashes: string
106+
listingChain: string
107+
protocolAddress: string
108+
fulfiller: string
109+
paymentChain: string
110+
paymentToken: string
111+
recipient?: string
112+
}) => {
113+
const client = getClient()
114+
const hashes = options.hashes.split(",").map(h => h.trim())
115+
const listings = hashes.map(hash => ({
116+
hash,
117+
chain: options.listingChain,
118+
protocol_address: options.protocolAddress,
119+
}))
120+
const body: Record<string, unknown> = {
121+
listings,
122+
fulfiller: { address: options.fulfiller },
123+
payment: {
124+
chain: options.paymentChain,
125+
token_address: options.paymentToken,
126+
},
127+
}
128+
if (options.recipient) {
129+
body.recipient = options.recipient
130+
}
131+
const result = await client.post<CrossChainFulfillmentDataResponse>(
132+
"/api/v2/listings/cross_chain_fulfillment_data",
133+
body,
134+
)
135+
console.log(formatOutput(result, getFormat()))
136+
},
137+
)
138+
72139
return cmd
73140
}

src/sdk.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
CollectionPaginatedResponse,
1414
CollectionStats,
1515
Contract,
16+
CrossChainFulfillmentDataResponse,
1617
DropDetailedResponse,
1718
DropMintResponse,
1819
DropPaginatedResponse,
@@ -345,6 +346,38 @@ class ListingsAPI {
345346
`/api/v2/listings/collection/${collectionSlug}/nfts/${tokenId}/best`,
346347
)
347348
}
349+
350+
async crossChainFulfillmentData(options: {
351+
listings: Array<{
352+
hash: string
353+
chain: string
354+
protocolAddress: string
355+
}>
356+
fulfillerAddress: string
357+
paymentChain: string
358+
paymentTokenAddress: string
359+
recipient?: string
360+
}): Promise<CrossChainFulfillmentDataResponse> {
361+
const body: Record<string, unknown> = {
362+
listings: options.listings.map(l => ({
363+
hash: l.hash,
364+
chain: l.chain,
365+
protocol_address: l.protocolAddress,
366+
})),
367+
fulfiller: { address: options.fulfillerAddress },
368+
payment: {
369+
chain: options.paymentChain,
370+
token_address: options.paymentTokenAddress,
371+
},
372+
}
373+
if (options.recipient) {
374+
body.recipient = options.recipient
375+
}
376+
return this.client.post(
377+
"/api/v2/listings/cross_chain_fulfillment_data",
378+
body,
379+
)
380+
}
348381
}
349382

350383
class OffersAPI {

src/types/api.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,17 @@ export type TokenBalanceSortBy =
311311
| "ONE_DAY_PRICE_CHANGE"
312312
| "SEVEN_DAY_PRICE_CHANGE"
313313

314+
export interface CrossChainFulfillmentTransaction {
315+
chain: string
316+
to: string
317+
data: string
318+
value: string
319+
}
320+
321+
export interface CrossChainFulfillmentDataResponse {
322+
transactions: CrossChainFulfillmentTransaction[]
323+
}
324+
314325
export interface ValidateMetadataResponse {
315326
assetIdentifier: {
316327
chain: string

test/commands/listings.test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ describe("listingsCommand", () => {
2020
expect(subcommands).toContain("all")
2121
expect(subcommands).toContain("best")
2222
expect(subcommands).toContain("best-for-nft")
23+
expect(subcommands).toContain("cross-chain-fulfill")
2324
})
2425

2526
it("all subcommand fetches all listings", async () => {
@@ -60,4 +61,115 @@ describe("listingsCommand", () => {
6061
"/api/v2/listings/collection/cool-cats/nfts/123/best",
6162
)
6263
})
64+
65+
it("cross-chain-fulfill subcommand posts correct body", async () => {
66+
ctx.mockClient.post.mockResolvedValue({ transactions: [] })
67+
68+
const cmd = listingsCommand(ctx.getClient, ctx.getFormat)
69+
await cmd.parseAsync(
70+
[
71+
"cross-chain-fulfill",
72+
"--hashes",
73+
"0xabc",
74+
"--listing-chain",
75+
"ethereum",
76+
"--protocol-address",
77+
"0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC",
78+
"--fulfiller",
79+
"0x1234567890abcdef1234567890abcdef12345678",
80+
"--payment-chain",
81+
"base",
82+
"--payment-token",
83+
"0x0000000000000000000000000000000000000000",
84+
],
85+
{ from: "user" },
86+
)
87+
88+
expect(ctx.mockClient.post).toHaveBeenCalledWith(
89+
"/api/v2/listings/cross_chain_fulfillment_data",
90+
{
91+
listings: [
92+
{
93+
hash: "0xabc",
94+
chain: "ethereum",
95+
protocol_address: "0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC",
96+
},
97+
],
98+
fulfiller: {
99+
address: "0x1234567890abcdef1234567890abcdef12345678",
100+
},
101+
payment: {
102+
chain: "base",
103+
token_address: "0x0000000000000000000000000000000000000000",
104+
},
105+
},
106+
)
107+
})
108+
109+
it("cross-chain-fulfill supports multiple hashes", async () => {
110+
ctx.mockClient.post.mockResolvedValue({ transactions: [] })
111+
112+
const cmd = listingsCommand(ctx.getClient, ctx.getFormat)
113+
await cmd.parseAsync(
114+
[
115+
"cross-chain-fulfill",
116+
"--hashes",
117+
"0xabc,0xdef",
118+
"--listing-chain",
119+
"ethereum",
120+
"--protocol-address",
121+
"0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC",
122+
"--fulfiller",
123+
"0x1234567890abcdef1234567890abcdef12345678",
124+
"--payment-chain",
125+
"base",
126+
"--payment-token",
127+
"0x0000000000000000000000000000000000000000",
128+
],
129+
{ from: "user" },
130+
)
131+
132+
expect(ctx.mockClient.post).toHaveBeenCalledWith(
133+
"/api/v2/listings/cross_chain_fulfillment_data",
134+
expect.objectContaining({
135+
listings: [
136+
expect.objectContaining({ hash: "0xabc" }),
137+
expect.objectContaining({ hash: "0xdef" }),
138+
],
139+
}),
140+
)
141+
})
142+
143+
it("cross-chain-fulfill passes optional recipient", async () => {
144+
ctx.mockClient.post.mockResolvedValue({ transactions: [] })
145+
146+
const cmd = listingsCommand(ctx.getClient, ctx.getFormat)
147+
await cmd.parseAsync(
148+
[
149+
"cross-chain-fulfill",
150+
"--hashes",
151+
"0xabc",
152+
"--listing-chain",
153+
"ethereum",
154+
"--protocol-address",
155+
"0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC",
156+
"--fulfiller",
157+
"0x1234567890abcdef1234567890abcdef12345678",
158+
"--payment-chain",
159+
"base",
160+
"--payment-token",
161+
"0x0000000000000000000000000000000000000000",
162+
"--recipient",
163+
"0xrecipient",
164+
],
165+
{ from: "user" },
166+
)
167+
168+
expect(ctx.mockClient.post).toHaveBeenCalledWith(
169+
"/api/v2/listings/cross_chain_fulfillment_data",
170+
expect.objectContaining({
171+
recipient: "0xrecipient",
172+
}),
173+
)
174+
})
63175
})

test/sdk.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,4 +597,68 @@ describe("OpenSeaCLI", () => {
597597
expect(result.message).toContain("Network error: fetch failed")
598598
})
599599
})
600+
601+
describe("listings.crossChainFulfillmentData", () => {
602+
it("posts to correct endpoint with snake_case body", async () => {
603+
mockPost.mockResolvedValue({
604+
transactions: [
605+
{ chain: "ethereum", to: "0xabc", data: "0x123", value: "0" },
606+
],
607+
})
608+
const result = await sdk.listings.crossChainFulfillmentData({
609+
listings: [
610+
{
611+
hash: "0xorderhash",
612+
chain: "ethereum",
613+
protocolAddress: "0xseaport",
614+
},
615+
],
616+
fulfillerAddress: "0xbuyer",
617+
paymentChain: "base",
618+
paymentTokenAddress: "0x0000000000000000000000000000000000000000",
619+
})
620+
expect(mockPost).toHaveBeenCalledWith(
621+
"/api/v2/listings/cross_chain_fulfillment_data",
622+
{
623+
listings: [
624+
{
625+
hash: "0xorderhash",
626+
chain: "ethereum",
627+
protocol_address: "0xseaport",
628+
},
629+
],
630+
fulfiller: { address: "0xbuyer" },
631+
payment: {
632+
chain: "base",
633+
token_address: "0x0000000000000000000000000000000000000000",
634+
},
635+
},
636+
)
637+
expect(result.transactions).toHaveLength(1)
638+
expect(result.transactions[0].chain).toBe("ethereum")
639+
})
640+
641+
it("includes recipient when provided", async () => {
642+
mockPost.mockResolvedValue({ transactions: [] })
643+
await sdk.listings.crossChainFulfillmentData({
644+
listings: [
645+
{
646+
hash: "0xhash",
647+
chain: "ethereum",
648+
protocolAddress: "0xseaport",
649+
},
650+
],
651+
fulfillerAddress: "0xbuyer",
652+
paymentChain: "base",
653+
paymentTokenAddress: "0x0000000000000000000000000000000000000000",
654+
recipient: "0xrecipient",
655+
})
656+
expect(mockPost).toHaveBeenCalledWith(
657+
"/api/v2/listings/cross_chain_fulfillment_data",
658+
expect.objectContaining({
659+
recipient: "0xrecipient",
660+
}),
661+
)
662+
})
663+
})
600664
})

0 commit comments

Comments
 (0)