@medialane/sdk 0.85.9 → 0.85.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **Framework-agnostic TypeScript SDK for the Medialane IP marketplace on Starknet**
6
6
 
7
- The Medialane SDK provides a unified interface for interacting with the Medialane marketplace both **on-chain operations** (create listings, make offers, fulfill orders, mint IP assets) and **REST API access** (search tokens, manage orders, upload metadata to IPFS). Built for [Medialane.io](https://medialane.io) and [Medialane.xyz](https://medialane.xyz).
7
+ The Medialane SDK provides a unified interface for interacting with the Medialane marketplace: both **on-chain operations** (create listings, make offers, fulfill orders, mint IP assets) and **REST API access** (search tokens, manage orders, upload metadata to IPFS). Built for [medialane.io](https://medialane.io), [starknet.medialane.io](https://starknet.medialane.io), [portal.medialane.io](https://portal.medialane.io), and `media-wallet`.
8
8
 
9
9
  ---
10
10
 
@@ -30,9 +30,9 @@ The Medialane SDK provides a unified interface for interacting with the Medialan
30
30
  - ERC-1155 multi-holder ownership via `token.balances`
31
31
 
32
32
  **IP Metadata Types**
33
- - `IpAttribute` typed OpenSea ERC-721 attribute
34
- - `IpNftMetadata` full IPFS metadata shape with licensing fields
35
- - `ApiTokenMetadata` indexed token metadata with all licensing attributes
33
+ - `IpAttribute`: typed OpenSea ERC-721 attribute
34
+ - `IpNftMetadata`: full IPFS metadata shape with licensing fields
35
+ - `ApiTokenMetadata`: indexed token metadata with all licensing attributes
36
36
  - Berne Convention-compatible licensing data model
37
37
 
38
38
  **Developer-Friendly**
@@ -75,11 +75,11 @@ const client = new MedialaneClient({
75
75
 
76
76
  ## Marketplace Operations (On-Chain)
77
77
 
78
- All methods require a `starknet.js` `AccountInterface`. SNIP-12 signing and `waitForTransaction` are handled automatically. Fulfilment is **unsigned** the caller is the fulfiller, so there is no `fulfiller`/`offerer` field to pass; cancellation still signs, but without a nonce (a per-offerer `counter` replaces it, see `incrementCounter`).
78
+ All methods require a `starknet.js` `AccountInterface`. SNIP-12 signing and `waitForTransaction` are handled automatically. Fulfilment is **unsigned**: the caller is the fulfiller, so there is no `fulfiller`/`offerer` field to pass; cancellation still signs, but without a nonce (a per-offerer `counter` replaces it, see `incrementCounter`).
79
79
 
80
80
  Two marketplace modules are available:
81
- - `client.marketplace` ERC-721 marketplace (`Medialane721`)
82
- - `client.marketplace1155` ERC-1155 marketplace (`Medialane1155`)
81
+ - `client.marketplace`: ERC-721 marketplace (`Medialane721`)
82
+ - `client.marketplace1155`: ERC-1155 marketplace (`Medialane1155`)
83
83
 
84
84
  ### Create a Listing (ERC-721)
85
85
 
@@ -141,7 +141,7 @@ const result = await client.marketplace.cancelOrder(account, {
141
141
  ### Bulk-Cancel (Invalidate All Open Orders)
142
142
 
143
143
  ```typescript
144
- // Bumps the caller's counter every previously-registered order becomes unfulfillable.
144
+ // Bumps the caller's counter: every previously-registered order becomes unfulfillable.
145
145
  await client.marketplace.incrementCounter(account);
146
146
  ```
147
147
 
@@ -170,7 +170,7 @@ const result = await client.marketplace.createCollection(account, {
170
170
 
171
171
  ## ERC-1155 Marketplace (Medialane1155)
172
172
 
173
- For IP assets from ERC-1155 collections (e.g. IP-Programmable-ERC1155-Collections). Contract address: see `getCoordinates("STARKNET").marketplace1155` in `src/chains.ts` (the single source do not hardcode it here, it changes on redeploy).
173
+ For IP assets from ERC-1155 collections (e.g. IP-Programmable-ERC1155-Collections). Contract address: read `getCoordinates("STARKNET").marketplace1155` from `src/chains.ts`, the single source of truth across redeploys.
174
174
 
175
175
  ### Create an ERC-1155 Listing
176
176
 
@@ -213,7 +213,7 @@ const result = await client.marketplace1155.cancelOrder(account, {
213
213
  ### SNIP-12 Typed Data Builders (custodial-wallet / custom flows)
214
214
 
215
215
  Listing/offer and cancellation are signed; fulfilment is an **unsigned** call (the buyer is
216
- the fulfiller, since v0.26.0) there is no fulfillment typed-data builder.
216
+ the fulfiller, since v0.26.0): there is no fulfillment typed-data builder.
217
217
 
218
218
  ```typescript
219
219
  import { build1155OrderTypedData, build1155CancellationTypedData } from "@medialane/sdk";
@@ -252,7 +252,7 @@ const history = await client.api.getTokenHistory(contract, tokenId);
252
252
 
253
253
  ### ERC-1155 Ownership
254
254
 
255
- For ERC-1155 tokens, a single token ID can be held by many wallets simultaneously. Use `token.balances` instead of `token.owner`:
255
+ For ERC-1155 tokens, a single token ID can be held by many wallets simultaneously; read ownership from `token.balances`:
256
256
 
257
257
  ```typescript
258
258
  import type { ApiTokenBalance } from "@medialane/sdk";
@@ -274,12 +274,12 @@ token.balances?.forEach((b: ApiTokenBalance) => {
274
274
  });
275
275
  ```
276
276
 
277
- `token.owner` is deprecated and always `null` post-migration. `token.balances` is only populated on single-token fetches (`getToken`) it is `null` on list responses.
277
+ `token.owner` is deprecated and always `null` post-migration. `token.balances` is only populated on single-token fetches (`getToken`): it is `null` on list responses.
278
278
 
279
279
  ### Query Collections
280
280
 
281
281
  ```typescript
282
- // All collections newest first by default
282
+ // All collections: newest first by default
283
283
  const collections = await client.api.getCollections();
284
284
 
285
285
  // With sort and pagination
@@ -295,9 +295,9 @@ const tokens = await client.api.getCollectionTokens(contract);
295
295
 
296
296
  ```typescript
297
297
  const results = await client.api.search("landscape painting", 10);
298
- // results.data.tokens matching tokens
299
- // results.data.collections matching collections
300
- // results.data.creators matching creator profiles (v0.4.5)
298
+ // results.data.tokens: matching tokens
299
+ // results.data.collections: matching collections
300
+ // results.data.creators: matching creator profiles (v0.4.5)
301
301
  ```
302
302
 
303
303
  ### Activities
@@ -351,7 +351,7 @@ const signature = await account.signMessage(intent.data.typedData);
351
351
  await client.api.submitIntentSignature(intent.data.id, toSignatureArray(signature));
352
352
  ```
353
353
 
354
- Mint and collection intents are pre-signed no signature step needed:
354
+ Mint and collection intents are pre-signed: no signature step needed:
355
355
 
356
356
  ```typescript
357
357
  const mintIntent = await client.api.createMintIntent({
@@ -393,7 +393,7 @@ const metadata: IpNftMetadata = {
393
393
  ],
394
394
  };
395
395
 
396
- // Token from the API includes indexed licensing fields for fast access
396
+ // Token from the API: includes indexed licensing fields for fast access
397
397
  const token = await client.api.getToken(contract, tokenId);
398
398
  token.data.metadata.licenseType; // "CC BY-NC-SA"
399
399
  token.data.metadata.commercialUse; // "No"
@@ -473,8 +473,8 @@ try {
473
473
  |---|---|---|---|
474
474
  | `chain` | `Chain` (`"STARKNET" \| "ETHEREUM" \| "SOLANA" \| "BASE" \| "BITCOIN"`) | `"STARKNET"` | The chain this client is scoped to. Coordinates resolve from the `coordinates[chain]` registry (`chains.ts`). Replaces `network` (v0.37.0). |
475
475
  | `rpcUrl` | `string` | the chain's registry `rpcUrl` | JSON-RPC URL override |
476
- | `backendUrl` | `string` | | Medialane API base URL (required for `.api.*`) |
477
- | `apiKey` | `string` | | API key from [Medialane Portal](https://medialane.xyz) |
476
+ | `backendUrl` | `string` | (none) | Medialane API base URL (required for `.api.*`) |
477
+ | `apiKey` | `string` | (none) | API key from [Medialane Portal](https://portal.medialane.io) |
478
478
  | `marketplace721Contract` | `string` | Mainnet default | ERC-721 marketplace protocol override |
479
479
  | `marketplaceContract` | `string` | Mainnet default | Legacy alias for `marketplace721Contract` |
480
480
  | `marketplace1155Contract` | `string` | Mainnet default | ERC-1155 marketplace protocol override |
@@ -511,9 +511,9 @@ bun run typecheck # tsc --noEmit
511
511
  ```
512
512
 
513
513
  Built with:
514
- - **tsup** dual ESM/CJS bundling
515
- - **TypeScript** full type safety
516
- - **Zod** runtime config validation
514
+ - **tsup**: dual ESM/CJS bundling
515
+ - **TypeScript**: full type safety
516
+ - **Zod**: runtime config validation
517
517
  - Peer dep: `starknet >= 6.0.0`
518
518
 
519
519
  ---
@@ -522,125 +522,125 @@ Built with:
522
522
 
523
523
  > Full history in [CHANGELOG.md](./CHANGELOG.md). Highlights below.
524
524
 
525
- ### v0.37.0 multichain readiness (BREAKING)
525
+ ### v0.37.0: multichain readiness (BREAKING)
526
526
  - **Chain is a first-class axis.** New `chains.ts` `coordinates[chain]` registry is the single source of per-chain service coordinates (`CHAINS`, `getCoordinates`, `DEFAULT_CHAIN`, `Chain`, `ChainCoordinates`); the flat `*_MAINNET` constants derive from it.
527
- - **`MedialaneConfig.chain` replaces `network`** the client is chain-scoped; `client.network` getter → `client.chain`.
528
- - **`ServiceDefinition.onchain` is per-chain** `Partial<Record<Chain, …>>`; read `service.onchain?.STARKNET?.factoryAddress`.
529
- - **`normalizeAddress(chain, address)`** per-chain codec (Starknet pad / EVM EIP-55 / Solana base58; Bitcoin not yet implemented).
530
- - **Removed** `SUPPORTED_NETWORKS`, `DEFAULT_RPC_URL`, `Network` (mainnet-only coordinates key by chain alone). `getChainId(config)` throws for non-Starknet.
527
+ - **`MedialaneConfig.chain` replaces `network`**: the client is chain-scoped; `client.network` getter → `client.chain`.
528
+ - **`ServiceDefinition.onchain` is per-chain**: `Partial<Record<Chain, …>>`; read `service.onchain?.STARKNET?.factoryAddress`.
529
+ - **`normalizeAddress(chain, address)`**: per-chain codec (Starknet pad / EVM EIP-55 / Solana base58; Bitcoin not yet implemented).
530
+ - **Removed** `SUPPORTED_NETWORKS`, `DEFAULT_RPC_URL`, `Network` (mainnet-only: coordinates key by chain alone). `getChainId(config)` throws for non-Starknet.
531
531
 
532
532
  ### v0.6.7
533
- - **`CollectionRegistryABI`** exported from `@medialane/sdk` minimal ABI covering `list_user_collections` and `get_collection` on the collection registry contract. Eliminates duplicated inline ABI definitions in consuming apps.
533
+ - **`CollectionRegistryABI`** exported from `@medialane/sdk`: minimal ABI covering `list_user_collections` and `get_collection` on the collection registry contract. Eliminates duplicated inline ABI definitions in consuming apps.
534
534
 
535
535
  ### v0.6.6
536
536
  - **`COLLECTION_CONTRACT_MAINNET`** updated to audited v2 contract address `0x05c49ee5d3208a2c2e150fdd0c247d1195ed9ab54fa2d5dea7a633f39e4b205b`
537
537
 
538
538
  ### v0.6.5
539
- - **ERC-1155 support** `ApiToken.balances: ApiTokenBalance[] | null` replaces the single `owner` field for ownership checks
540
- - **`ApiTokenBalance`** type `{ owner: string; amount: string }` each entry represents one holder and their quantity
541
- - **`ApiToken.owner`** deprecated always `null` after the ERC-1155 migration; use `balances` instead
542
- - **`ApiCollection.standard`** `"ERC721" | "ERC1155" | "UNKNOWN"` detected via ERC-165 `supportsInterface`
543
- - **`totalSupply` fix** ERC-1155 collections now report `SUM(holder amounts)` instead of distinct token ID count
539
+ - **ERC-1155 support**: `ApiToken.balances: ApiTokenBalance[] | null` replaces the single `owner` field for ownership checks
540
+ - **`ApiTokenBalance`** type: `{ owner: string; amount: string }`: each entry represents one holder and their quantity
541
+ - **`ApiToken.owner`** deprecated: always `null` after the ERC-1155 migration; use `balances` instead
542
+ - **`ApiCollection.standard`**: `"ERC721" | "ERC1155" | "UNKNOWN"` detected via ERC-165 `supportsInterface`
543
+ - **`totalSupply` fix**: ERC-1155 collections now report `SUM(holder amounts)` for an accurate circulating total
544
544
 
545
545
  ### v0.6.1
546
- - **Collection Drop** new `DropService` (`client.services.drop`) with full on-chain drop management: `claim`, `adminMint`, `setClaimConditions`, `setAllowlistEnabled`, `addToAllowlist`, `batchAddToAllowlist`, `setPaused`, `withdrawPayments`, `createDrop`
547
- - **`client.api.getDropCollections(opts?)`** list all `COLLECTION_DROP` collections
548
- - **`client.api.getDropMintStatus(collection, wallet)`** returns `{ mintedByWallet, totalMinted }`
546
+ - **Collection Drop**: new `DropService` (`client.services.drop`) with full on-chain drop management: `claim`, `adminMint`, `setClaimConditions`, `setAllowlistEnabled`, `addToAllowlist`, `batchAddToAllowlist`, `setPaused`, `withdrawPayments`, `createDrop`
547
+ - **`client.api.getDropCollections(opts?)`**: list all `COLLECTION_DROP` collections
548
+ - **`client.api.getDropMintStatus(collection, wallet)`**: returns `{ mintedByWallet, totalMinted }`
549
549
  - **`DropMintStatus`**, **`ClaimConditions`**, **`CreateDropParams`** types exported
550
550
  - **`DropCollectionABI`** and **`DropFactoryABI`** exported from `@medialane/sdk`
551
551
  - **`DROP_FACTORY_CONTRACT_MAINNET`** and **`DROP_COLLECTION_CLASS_HASH_MAINNET`** constants exported
552
552
  - **`CollectionSource`** union extended with `"COLLECTION_DROP"`
553
553
 
554
554
  ### v0.6.0
555
- - **POP Protocol** `PopService` (`client.services.pop`): `claim`, `adminMint`, `addToAllowlist`, `batchAddToAllowlist`, `removeFromAllowlist`, `setTokenUri`, `setPaused`, `createCollection`
555
+ - **POP Protocol**: `PopService` (`client.services.pop`): `claim`, `adminMint`, `addToAllowlist`, `batchAddToAllowlist`, `removeFromAllowlist`, `setTokenUri`, `setPaused`, `createCollection`
556
556
  - **`client.api.getPopCollections(opts?)`** and **`client.api.getPopEligibility(collection, wallet)`**
557
557
  - **`POPCollectionABI`** and **`POPFactoryABI`** exported
558
558
  - **`POP_FACTORY_CONTRACT_MAINNET`** and **`POP_COLLECTION_CLASS_HASH_MAINNET`** constants exported
559
559
 
560
560
  ### v0.5.7
561
- - **`ApiCollectionProfile.hasGatedContent: boolean`** whether the collection has token-gated content configured
562
- - **`ApiCollectionProfile.gatedContentTitle: string | null`** public title of gated content (shown to all users; URL is accessible to holders only via the backend gated-content endpoint)
561
+ - **`ApiCollectionProfile.hasGatedContent: boolean`**: whether the collection has token-gated content configured
562
+ - **`ApiCollectionProfile.gatedContentTitle: string | null`**: public title of gated content (shown to all users; URL is accessible to holders only via the backend gated-content endpoint)
563
563
 
564
564
  ### v0.5.5
565
- - **`extendRemixOffer(id, days, siwsToken)`** requester extends expiry of a PENDING/AUTO_PENDING remix offer by 1–30 days (`POST /v1/remix-offers/:id/extend`)
566
- - **`ApiRemixOfferPrice`** type `{ raw, formatted, currency, decimals }` replaces flat `proposedPrice`/`proposedCurrency` fields on `ApiRemixOffer.price` (visible to participants only)
565
+ - **`extendRemixOffer(id, days, siwsToken)`**: requester extends expiry of a PENDING/AUTO_PENDING remix offer by 1–30 days (`POST /v1/remix-offers/:id/extend`)
566
+ - **`ApiRemixOfferPrice`** type: `{ raw, formatted, currency, decimals }` replaces flat `proposedPrice`/`proposedCurrency` fields on `ApiRemixOffer.price` (visible to participants only)
567
567
 
568
568
  ### v0.5.4
569
- - **`ApiRemixOffer.price`** shape introduced backend now serializes price as a structured object (`raw`, `formatted`, `currency`, `decimals`) instead of raw wei strings
569
+ - **`ApiRemixOffer.price`** shape introduced: backend now serializes price as a structured object (`raw`, `formatted`, `currency`, `decimals`), replacing raw wei strings
570
570
 
571
571
  ### v0.5.3
572
- - **`getTokenComments(contract, tokenId, opts?)`** fetch on-chain NFT comments for a token (`GET /v1/tokens/:contract/:tokenId/comments`)
573
- - **`ApiComment`** type `{ id, author, content, txHash, blockNumber, blockTimestamp, isHidden, createdAt }`
572
+ - **`getTokenComments(contract, tokenId, opts?)`**: fetch on-chain NFT comments for a token (`GET /v1/tokens/:contract/:tokenId/comments`)
573
+ - **`ApiComment`** type: `{ id, author, content, txHash, blockNumber, blockTimestamp, isHidden, createdAt }`
574
574
 
575
575
  ### v0.5.0
576
- - **Counter-offer support** `createCounterOfferIntent(params, siwsToken)`, `getCounterOffers(query)`, `ApiCounterOffersQuery`, `CreateCounterOfferIntentParams`
576
+ - **Counter-offer support**: `createCounterOfferIntent(params, siwsToken)`, `getCounterOffers(query)`, `ApiCounterOffersQuery`, `CreateCounterOfferIntentParams`
577
577
  - **`OrderStatus`** extended with `"COUNTER_OFFERED"`; **`IntentType`** with `"COUNTER_OFFER"`
578
578
  - **`ApiOrder`** extended: `parentOrderHash?: string | null`, `counterOfferMessage?: string | null`
579
- - **Remix licensing** full set of remix offer methods and types:
580
- - `submitRemixOffer(params, siwsToken)` custom offer
581
- - `submitAutoRemixOffer(params, siwsToken)` auto offer for open-license tokens
582
- - `confirmSelfRemix(params, siwsToken)` record owner self-remix
583
- - `getRemixOffers(query, siwsToken)` list by role
584
- - `getRemixOffer(id, siwsToken?)` single offer
585
- - `confirmRemixOffer(id, params, siwsToken)` creator approves
586
- - `rejectRemixOffer(id, siwsToken)` creator rejects
587
- - `getTokenRemixes(contract, tokenId, opts?)` public remix list
588
- - **New types** `RemixOfferStatus`, `ApiRemixOffer`, `ApiPublicRemix`, `OPEN_LICENSES`, `OpenLicense`, `CreateRemixOfferParams`, `AutoRemixOfferParams`, `ConfirmSelfRemixParams`, `ConfirmRemixOfferParams`, `ApiRemixOffersQuery`
579
+ - **Remix licensing**: full set of remix offer methods and types:
580
+ - `submitRemixOffer(params, siwsToken)`: custom offer
581
+ - `submitAutoRemixOffer(params, siwsToken)`: auto offer for open-license tokens
582
+ - `confirmSelfRemix(params, siwsToken)`: record owner self-remix
583
+ - `getRemixOffers(query, siwsToken)`: list by role
584
+ - `getRemixOffer(id, siwsToken?)`: single offer
585
+ - `confirmRemixOffer(id, params, siwsToken)`: creator approves
586
+ - `rejectRemixOffer(id, siwsToken)`: creator rejects
587
+ - `getTokenRemixes(contract, tokenId, opts?)`: public remix list
588
+ - **New types**: `RemixOfferStatus`, `ApiRemixOffer`, `ApiPublicRemix`, `OPEN_LICENSES`, `OpenLicense`, `CreateRemixOfferParams`, `AutoRemixOfferParams`, `ConfirmSelfRemixParams`, `ConfirmRemixOfferParams`, `ApiRemixOffersQuery`
589
589
 
590
590
  ### v0.4.8
591
591
  - **`ApiComment`** type + **`getTokenComments`** (patch release, backported into v0.5.3)
592
592
 
593
593
  ### v0.4.7
594
- - **`IPType`** union type exported `"Audio" | "Art" | "Documents" | "NFT" | "Video" | "Photography" | "Patents" | "Posts" | "Publications" | "RWA" | "Software" | "Custom"`
594
+ - **`IPType`** union type exported: `"Audio" | "Art" | "Documents" | "NFT" | "Video" | "Photography" | "Patents" | "Posts" | "Publications" | "RWA" | "Software" | "Custom"`
595
595
 
596
596
  ### v0.4.6
597
597
  - **`ApiUserWallet`** type + `upsertMyWallet(siwsToken)` / `getMyWallet(siwsToken)` for wallet registration fallback (`POST/GET /v1/users/me`)
598
598
 
599
599
  ### v0.4.5
600
- - **`ApiSearchCreatorResult`** type + `ApiSearchResult.creators` creator profiles now included in search results
600
+ - **`ApiSearchCreatorResult`** type + `ApiSearchResult.creators`: creator profiles now included in search results
601
601
 
602
602
  ### v0.4.4
603
- - **`ApiCreatorListResult`** + `getCreators(opts?)` list creators with search/pagination via `GET /v1/creators`
603
+ - **`ApiCreatorListResult`** + `getCreators(opts?)`: list creators with search/pagination via `GET /v1/creators`
604
604
 
605
605
  ### v0.4.3
606
- - **`ApiCreatorProfile.username`** field + `getCreatorByUsername(username)` resolve username slug to creator profile
606
+ - **`ApiCreatorProfile.username`** field + `getCreatorByUsername(username)`: resolve username slug to creator profile
607
607
 
608
608
  ### v0.4.2
609
609
  - **WBTC** added to `SUPPORTED_TOKENS` (`0x03fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac`, 8 decimals)
610
- - **`listable` field** on every `SUPPORTED_TOKENS` entry controls whether a token appears in listing/offer dialogs vs filter-only
611
- - **`getListableTokens()`** returns tokens filtered to `listable: true`; exported from package root
612
- - **ETH** promoted to `listable: true` now available in listing and offer dialogs
613
- - **USDC.e removed** bridged USDC (`0x053c91...`) removed entirely; only Circle-native USDC remains, to avoid user confusion
610
+ - **`listable` field** on every `SUPPORTED_TOKENS` entry: controls whether a token appears in listing/offer dialogs vs filter-only
611
+ - **`getListableTokens()`**: returns tokens filtered to `listable: true`; exported from package root
612
+ - **ETH** promoted to `listable: true`: now available in listing and offer dialogs
613
+ - **USDC.e removed**: bridged USDC (`0x053c91...`) removed entirely; only Circle-native USDC remains, to avoid user confusion
614
614
 
615
615
  ### v0.4.1
616
- - **Collection claims** `claimCollection(contractAddress, walletAddress, siwsToken)` for on-chain ownership verification; `requestCollectionClaim({ contractAddress, walletAddress?, email, notes? })` for manual review
617
- - **Collection profiles** `getCollectionProfile(contractAddress)` and `updateCollectionProfile(contractAddress, data, siwsToken)` for enriched display metadata (displayName, description, image, bannerImage, social links)
618
- - **Creator profiles** `getCreatorProfile(walletAddress)` and `updateCreatorProfile(walletAddress, data, siwsToken)` for creator display metadata
619
- - **New types** `ApiCollectionClaim`, `ApiAdminCollectionClaim`, `ApiCollectionProfile`, `ApiCreatorProfile`
616
+ - **Collection claims**: `claimCollection(contractAddress, walletAddress, siwsToken)` for on-chain ownership verification; `requestCollectionClaim({ contractAddress, walletAddress?, email, notes? })` for manual review
617
+ - **Collection profiles**: `getCollectionProfile(contractAddress)` and `updateCollectionProfile(contractAddress, data, siwsToken)` for enriched display metadata (displayName, description, image, bannerImage, social links)
618
+ - **Creator profiles**: `getCreatorProfile(walletAddress)` and `updateCreatorProfile(walletAddress, data, siwsToken)` for creator display metadata
619
+ - **New types**: `ApiCollectionClaim`, `ApiAdminCollectionClaim`, `ApiCollectionProfile`, `ApiCreatorProfile`
620
620
  - **`ApiCollection`** extended with `source` (`"MEDIALANE_REGISTRY" | "EXTERNAL" | "PARTNERSHIP" | "IP_TICKET" | "IP_CLUB" | "GAME"`) and `claimedBy: string | null`
621
621
  - `profile?: ApiCollectionProfile | null` optionally embedded on `ApiCollection` when `?include=profile`
622
622
 
623
623
  ### v0.4.0
624
- - **Typed error codes** `MedialaneError` and `MedialaneApiError` now expose a `.code: MedialaneErrorCode` property (`"TOKEN_NOT_FOUND"` | `"RATE_LIMITED"` | `"INTENT_EXPIRED"` | `"UNAUTHORIZED"` | `"INVALID_PARAMS"` | `"NETWORK_NOT_SUPPORTED"` | `"UNKNOWN"`)
625
- - **Automatic retry** all API requests retry up to 3 times with exponential backoff (300ms base, 5s cap); 4xx errors are not retried. Configure via `retryOptions` in `MedialaneConfig`
624
+ - **Typed error codes**: `MedialaneError` and `MedialaneApiError` now expose a `.code: MedialaneErrorCode` property (`"TOKEN_NOT_FOUND"` | `"RATE_LIMITED"` | `"INTENT_EXPIRED"` | `"UNAUTHORIZED"` | `"INVALID_PARAMS"` | `"NETWORK_NOT_SUPPORTED"` | `"UNKNOWN"`)
625
+ - **Automatic retry**: all API requests retry up to 3 times with exponential backoff (300ms base, 5s cap) on transient failures. Configure via `retryOptions` in `MedialaneConfig`
626
626
  - **`RetryOptions`** type exported from index
627
627
  - **`CollectionSort`** named union type exported (`"recent" | "supply" | "floor" | "volume" | "name"`)
628
- - **Sepolia guard** constructing a client with `network: "sepolia"` and no explicit contract addresses now throws `NETWORK_NOT_SUPPORTED` immediately
628
+ - **Sepolia guard**: constructing a client with `network: "sepolia"` and no explicit contract addresses now throws `NETWORK_NOT_SUPPORTED` immediately
629
629
 
630
630
  ### v0.3.3
631
- - `getCollections(page?, limit?, isKnown?, sort?)` added `sort` parameter: `"recent"` (default) | `"supply"` | `"floor"` | `"volume"` | `"name"`
632
- - Default sort changed from `totalSupply DESC` to `createdAt DESC` (newest first) matches backend default
631
+ - `getCollections(page?, limit?, isKnown?, sort?)`: added `sort` parameter: `"recent"` (default) | `"supply"` | `"floor"` | `"volume"` | `"name"`
632
+ - Default sort changed from `totalSupply DESC` to `createdAt DESC` (newest first): matches backend default
633
633
 
634
634
  ### v0.3.1
635
- - `ApiCollection.collectionId: string | null` on-chain registry numeric ID (decimal string). Required for `createMintIntent`. Populated for collections indexed after 2026-03-09.
635
+ - `ApiCollection.collectionId: string | null`: on-chain registry numeric ID (decimal string). Required for `createMintIntent`. Populated for collections indexed after 2026-03-09.
636
636
 
637
637
  ### v0.3.0
638
- - `normalizeAddress()` applied internally before all API calls callers no longer need to normalize Starknet addresses
639
- - `ApiCollection.owner: string | null` populated from intent typedData or on-chain `owner()` call
640
- - `getCollectionsByOwner(owner)` fetch collections by wallet address via `GET /v1/collections?owner=`
638
+ - `normalizeAddress()` applied internally before all API calls: callers no longer need to normalize Starknet addresses
639
+ - `ApiCollection.owner: string | null`: populated from intent typedData or on-chain `owner()` call
640
+ - `getCollectionsByOwner(owner)`: fetch collections by wallet address via `GET /v1/collections?owner=`
641
641
 
642
642
  ### v0.2.6
643
- - `ApiOrder.token: ApiOrderTokenMeta | null` token name/image/description embedded on orders (batchTokenMeta); no per-row `getToken` calls needed
643
+ - `ApiOrder.token: ApiOrderTokenMeta | null`: token name/image/description embedded on orders (batchTokenMeta); no per-row `getToken` calls needed
644
644
 
645
645
  ### v0.2.0
646
646
  - `IpAttribute` and `IpNftMetadata` interfaces for IP metadata
@@ -649,14 +649,15 @@ Built with:
649
649
  - Added `USDC.e` (bridged USDC via Starkgate) to `SUPPORTED_TOKENS`
650
650
 
651
651
  ### v0.1.0
652
- - Initial release orders, tokens, collections, activities, intents, metadata, portal
652
+ - Initial release: orders, tokens, collections, activities, intents, metadata, portal
653
653
 
654
654
  ---
655
655
 
656
656
  ## Links
657
657
 
658
658
  - **Marketplace**: [medialane.io](https://medialane.io)
659
- - **Developer Portal**: [medialane.xyz](https://medialane.xyz)
659
+ - **Starknet App**: [starknet.medialane.io](https://starknet.medialane.io)
660
+ - **Developer Portal**: [portal.medialane.io](https://portal.medialane.io)
660
661
  - **npm**: [npmjs.com/package/@medialane/sdk](https://www.npmjs.com/package/@medialane/sdk)
661
662
  - **GitHub**: [github.com/medialane-io](https://github.com/medialane-io)
662
663
 
package/dist/index.cjs CHANGED
@@ -1281,6 +1281,151 @@ function createFailoverFetch(urls, options = {}) {
1281
1281
  return failover;
1282
1282
  }
1283
1283
 
1284
+ // src/server/rpc-proxy.ts
1285
+ var DEFAULT_STARKNET_RPC_METHODS = [
1286
+ "starknet_call",
1287
+ "starknet_addInvokeTransaction",
1288
+ "starknet_getTransactionReceipt",
1289
+ "starknet_getTransactionStatus",
1290
+ "starknet_getTransactionByHash",
1291
+ "starknet_getTransaction",
1292
+ "starknet_getBlockWithReceipts",
1293
+ "starknet_estimateFee",
1294
+ "starknet_getNonce",
1295
+ "starknet_simulateTransactions",
1296
+ "starknet_specVersion",
1297
+ "starknet_chainId",
1298
+ "starknet_blockNumber",
1299
+ "starknet_blockHashAndNumber",
1300
+ "starknet_getClassAt",
1301
+ "starknet_getClass",
1302
+ "starknet_getClassHashAt",
1303
+ "starknet_getStorageAt",
1304
+ "starknet_getBlockWithTxHashes",
1305
+ "starknet_getBlockWithTxs",
1306
+ "starknet_getEvents"
1307
+ ];
1308
+ function isAllowedMethod(body, allowed) {
1309
+ if (Array.isArray(body)) {
1310
+ return body.every((item) => isAllowedMethod(item, allowed));
1311
+ }
1312
+ if (body && typeof body === "object") {
1313
+ const method = body.method;
1314
+ return typeof method === "string" && allowed.has(method);
1315
+ }
1316
+ return false;
1317
+ }
1318
+ function extractMethod(body) {
1319
+ if (Array.isArray(body)) return "batch";
1320
+ if (body && typeof body === "object") {
1321
+ const method = body.method;
1322
+ if (typeof method === "string") return method;
1323
+ }
1324
+ return "unknown";
1325
+ }
1326
+ function isSameOrigin(req) {
1327
+ const origin = req.headers.get("origin");
1328
+ if (!origin) return true;
1329
+ const host = req.headers.get("host");
1330
+ try {
1331
+ return new URL(origin).host === host;
1332
+ } catch {
1333
+ return false;
1334
+ }
1335
+ }
1336
+ function rpcError(code, message, status = 200, id = null) {
1337
+ return Response.json({ jsonrpc: "2.0", error: { code, message }, id }, { status });
1338
+ }
1339
+ async function billRpcCall(backendUrl, apiKey, method) {
1340
+ if (!apiKey) {
1341
+ console.error("[rpc-proxy] no API key configured \u2014 refusing to bill/forward");
1342
+ return false;
1343
+ }
1344
+ try {
1345
+ const res = await fetch(`${backendUrl.replace(/\/$/, "")}/v1/rpc/meter`, {
1346
+ method: "POST",
1347
+ headers: { "Content-Type": "application/json", "x-api-key": apiKey },
1348
+ body: JSON.stringify({ method })
1349
+ });
1350
+ return res.ok;
1351
+ } catch (err) {
1352
+ console.error("[rpc-proxy] billing call failed", { err: err instanceof Error ? err.message : String(err) });
1353
+ return false;
1354
+ }
1355
+ }
1356
+ function createRpcProxyHandler(config) {
1357
+ const allowed = new Set(config.allowedMethods ?? DEFAULT_STARKNET_RPC_METHODS);
1358
+ return async function handleRpcProxy(req) {
1359
+ if (!isSameOrigin(req)) {
1360
+ return rpcError(-32600, "Cross-origin requests are not allowed", 403);
1361
+ }
1362
+ const ip = req.headers.get("x-forwarded-for")?.split(",")[0].trim() ?? "unknown";
1363
+ if (!config.checkRateLimit(ip)) {
1364
+ return rpcError(-32005, "Too many requests", 429);
1365
+ }
1366
+ let body;
1367
+ try {
1368
+ body = await req.json();
1369
+ } catch {
1370
+ return rpcError(-32700, "Parse error");
1371
+ }
1372
+ if (!isAllowedMethod(body, allowed)) {
1373
+ const method2 = !Array.isArray(body) && body && typeof body === "object" ? String(body.method ?? "<unknown>") : "<batch or invalid>";
1374
+ return rpcError(-32601, `Method not allowed: ${method2}`);
1375
+ }
1376
+ const method = extractMethod(body);
1377
+ if (!await billRpcCall(config.backendUrl, config.apiKey, method)) {
1378
+ return rpcError(-32003, "Insufficient credits or billing unavailable \u2014 RPC call not forwarded", 402);
1379
+ }
1380
+ let lastError = "No RPC upstream configured";
1381
+ for (const rpcUrl of config.rpcUrls) {
1382
+ try {
1383
+ const response = await fetch(rpcUrl, {
1384
+ method: "POST",
1385
+ headers: { "Content-Type": "application/json" },
1386
+ body: JSON.stringify(body)
1387
+ });
1388
+ const text = await response.text();
1389
+ const upstream = rpcUrl.split("/")[2];
1390
+ if (!text) {
1391
+ lastError = `Upstream RPC returned empty body (HTTP ${response.status})`;
1392
+ console.warn("[rpc-proxy] upstream returned empty body", { status: response.status, upstream });
1393
+ continue;
1394
+ }
1395
+ try {
1396
+ const data = JSON.parse(text);
1397
+ if (isTransientRpcError({ status: response.status, body: data })) {
1398
+ const errObj = data.error;
1399
+ lastError = `Upstream RPC returned transient JSON-RPC error: ${String(errObj?.message ?? "(no message)")}`;
1400
+ console.warn("[rpc-proxy] upstream returned transient JSON-RPC error", {
1401
+ upstream,
1402
+ code: errObj?.code,
1403
+ message: errObj?.message
1404
+ });
1405
+ continue;
1406
+ }
1407
+ return Response.json(data, { status: 200 });
1408
+ } catch {
1409
+ lastError = `Upstream RPC returned non-JSON (HTTP ${response.status})`;
1410
+ console.warn("[rpc-proxy] upstream returned non-JSON", {
1411
+ status: response.status,
1412
+ upstream,
1413
+ bodyPreview: text.slice(0, 200)
1414
+ });
1415
+ continue;
1416
+ }
1417
+ } catch (err) {
1418
+ lastError = `Upstream RPC unreachable: ${err instanceof Error ? err.message : "unknown error"}`;
1419
+ console.warn("[rpc-proxy] upstream fetch failed", {
1420
+ upstream: rpcUrl.split("/")[2],
1421
+ err: err instanceof Error ? err.message : String(err)
1422
+ });
1423
+ }
1424
+ }
1425
+ return rpcError(-32603, lastError);
1426
+ };
1427
+ }
1428
+
1284
1429
  // src/metadata.ts
1285
1430
  var RESERVED_TRAITS = /* @__PURE__ */ new Set([
1286
1431
  "Creator",
@@ -1335,11 +1480,40 @@ function buildAssetMetadata(input) {
1335
1480
  };
1336
1481
  }
1337
1482
 
1483
+ // src/utils/ipfs-gateway.ts
1484
+ var CID_PATH_PATTERN = /^(Qm[1-9A-HJ-NP-Za-km-z]{44,}|b[a-z2-7]{58,})(\/[\w.\-/]*)?$/;
1485
+ function isValidIpfsCidPath(cidPath) {
1486
+ if (!CID_PATH_PATTERN.test(cidPath)) return false;
1487
+ if (cidPath.split("/").includes("..")) return false;
1488
+ return true;
1489
+ }
1490
+ var IPFS_SAFE_CONTENT_TYPE_PREFIXES = [
1491
+ "image/jpeg",
1492
+ "image/png",
1493
+ "image/gif",
1494
+ "image/webp",
1495
+ "image/avif",
1496
+ "image/svg+xml",
1497
+ "video/",
1498
+ "audio/",
1499
+ "model/",
1500
+ "font/",
1501
+ "application/json",
1502
+ "application/octet-stream"
1503
+ ];
1504
+ function resolveSafeImageContentType(contentType) {
1505
+ return IPFS_SAFE_CONTENT_TYPE_PREFIXES.some((p) => contentType.startsWith(p)) ? contentType : "application/octet-stream";
1506
+ }
1507
+ var MAX_IPFS_GATEWAY_RESPONSE_BYTES = 25 * 1024 * 1024;
1508
+
1338
1509
  exports.ApiClient = ApiClient;
1339
1510
  exports.CHAINS = CHAINS;
1340
1511
  exports.DEFAULT_CHAIN = DEFAULT_CHAIN;
1341
1512
  exports.DEFAULT_CURRENCY = DEFAULT_CURRENCY;
1513
+ exports.DEFAULT_STARKNET_RPC_METHODS = DEFAULT_STARKNET_RPC_METHODS;
1342
1514
  exports.FeeConfigSchema = FeeConfigSchema;
1515
+ exports.IPFS_SAFE_CONTENT_TYPE_PREFIXES = IPFS_SAFE_CONTENT_TYPE_PREFIXES;
1516
+ exports.MAX_IPFS_GATEWAY_RESPONSE_BYTES = MAX_IPFS_GATEWAY_RESPONSE_BYTES;
1343
1517
  exports.MedialaneApiError = MedialaneApiError;
1344
1518
  exports.OPEN_LICENSES = OPEN_LICENSES;
1345
1519
  exports.PUBLIC_RPC_FALLBACKS = PUBLIC_RPC_FALLBACKS;
@@ -1384,6 +1558,7 @@ exports.chainSlug = chainSlug;
1384
1558
  exports.coinHref = coinHref;
1385
1559
  exports.collectionHref = collectionHref;
1386
1560
  exports.createFailoverFetch = createFailoverFetch;
1561
+ exports.createRpcProxyHandler = createRpcProxyHandler;
1387
1562
  exports.encodeU256 = encodeU256;
1388
1563
  exports.formatAmount = formatAmount;
1389
1564
  exports.getCoordinates = getCoordinates;
@@ -1396,12 +1571,14 @@ exports.getTokenBySymbol = getTokenBySymbol;
1396
1571
  exports.hasCapability = hasCapability;
1397
1572
  exports.isServiceId = isServiceId;
1398
1573
  exports.isTransientRpcError = isTransientRpcError;
1574
+ exports.isValidIpfsCidPath = isValidIpfsCidPath;
1399
1575
  exports.listServices = listServices;
1400
1576
  exports.normalizeAddress = normalizeAddress;
1401
1577
  exports.normalizeHash = normalizeHash;
1402
1578
  exports.parseAmount = parseAmount;
1403
1579
  exports.resolveConfig = resolveConfig;
1404
1580
  exports.resolveFeeConfig = resolveFeeConfig;
1581
+ exports.resolveSafeImageContentType = resolveSafeImageContentType;
1405
1582
  exports.shortenAddress = shortenAddress;
1406
1583
  exports.stringifyBigInts = stringifyBigInts;
1407
1584
  exports.u256ToBigInt = u256ToBigInt;