Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,79 @@ AtomicAssets library for Wharf
yarn add @wharfkit/atomicassets
```

## AtomicAssets v2 and AtomicMarket v2

The v2 contracts add fields that the API returns only from a v2 indexer. Every one of them is
declared optional, so a response from a v1 indexer decodes unchanged and the field reads back
empty.

| Field | Returned on | Meaning |
| --- | --- | --- |
| `current_collection_fee` | sales, auctions, buyoffers, template buyoffers | The collection's `market_fee` as of the last indexed block. The nested `collection.market_fee` is the listing-time snapshot, and the two differ while the listing is open. |
| `types` | schemas | The media type descriptors authored through `setschematyp`, exactly as stored. |
| `mutable_data`, `data` | templates | The data written by `settempldata`, and the immutable and mutable data merged. |
| `deleted_at_block`, `deleted_at_time` | templates | Set once `deltemplate` removes the template. |
| `new_author_name`, `new_author_date` | collections | The pending author succession written by `createauswap`, and the date it may be accepted. |

A schema also reports `mediatype` and `info` inside `format[]`, but the API merges the authored
descriptors with a name and type heuristic there, so a reader cannot tell a stored value from a
derived one. `types` reports the authored ones alone. This matters to a client that calls
`setschematyp`, which replaces the whole array: resubmitting a `format[]` value writes the
heuristic's guess to chain. An empty `types` array means the schema has no descriptors, and an
absent one means the response does not report them, which is what a schema nested inside an asset
or template answers.

### Royalties

The AtomicMarket v2 royalty tables and the settled payout ledger are read through the v1 market
client. The example needs an indexer of an AtomicMarket v2 chain, such as the WAX testnet one
shown below:

```ts
import {APIClient, FetchProvider} from '@wharfkit/antelope'
import {AtomicAssetsAPIClient} from '@wharfkit/atomicassets'

const api = new AtomicAssetsAPIClient(
new APIClient({provider: new FetchProvider('https://test.wax.api.atomicassets.io')})
)
const market = api.atomicmarket.v1

// The royaltyconf, royaltytemp, and royaltyattr rows of one collection.
const config = await market.get_royalty_config('mycollection')
const templateRules = await market.get_royalty_template_rules('mycollection')
const attributeRules = await market.get_royalty_attribute_rules('mycollection')

// The payouts an account has been paid, the total number of them, and the
// per-token totals.
const payouts = await market.get_royalty_payouts({recipient: ['myaccount'], limit: 100})
const payoutCount = await market.get_royalty_payouts_count({recipient: ['myaccount']})
const totals = await market.get_royalty_account('myaccount')
```

A payout carries `amount` in the token's smallest unit, `category` naming which rule paid
(`founders`, `template`, `attribute`, or `dust`), and a nullable `listing_id`, `asset_id`,
`template_id`, and `rule_id`. `get_royalty_account` answers one row per token symbol.

A collection with no royalty configuration answers HTTP 416, which reaches the caller as an
`APIError` with `error.response.status === 416`. On an AtomicMarket v1 chain every collection
answers that way, and the payout routes answer an empty array. An indexer without the royalty
routes answers 404 on all six methods.

## Running Tests

```
make test
```

The suite never contacts the network. `@wharfkit/mock-data` replays recorded responses from
`test/data`, where each filename is a hash of the full request URL and its parameters, so
responses from two hosts never collide. To re-record one fixture, name its test:

```
MOCK=overwrite make test grep='get_royalty_payouts'
```

`MOCK=overwrite make test` without a `grep=` re-records every fixture in `test/data` against the
live hosts. The royalty fixtures come from `V2_BASE_URL` in `test/config.ts`, an AtomicMarket v2
indexer, because the default host indexes an AtomicMarket v1 chain where those routes answer 416
or an empty array.
54 changes: 34 additions & 20 deletions src/endpoints/assets/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {ActionNames as ActionType} from '../../contracts/atomicassets'
import {CountResponseStruct} from '../../types'
import * as Assets from './types'
import type {OfferState} from '../../types'
import {buildBodyParams} from '../utils'
import {buildBodyParams, pathSegment} from '../utils'

export interface GetAssetsOptions {
collection_name?: NameType[]
Expand Down Expand Up @@ -233,15 +233,15 @@ export class AssetsV1APIClient {

async get_asset(asset_id: UInt64Type) {
return this.client.call({
path: `/atomicassets/v1/assets/${asset_id}`,
path: `/atomicassets/v1/assets/${pathSegment(asset_id)}`,
method: 'GET',
responseType: Assets.GetAssetResponse,
})
}

async get_asset_stats(asset_id: UInt64Type) {
return this.client.call({
path: `/atomicassets/v1/assets/${asset_id}/stats`,
path: `/atomicassets/v1/assets/${pathSegment(asset_id)}/stats`,
method: 'GET',
responseType: Assets.GetAssetStatsResponse,
})
Expand All @@ -260,7 +260,7 @@ export class AssetsV1APIClient {
const bodyParams = buildBodyParams(options)

return this.client.call({
path: `/atomicassets/v1/assets/${asset_id}/logs`,
path: `/atomicassets/v1/assets/${pathSegment(asset_id)}/logs`,
method: 'POST',
params: bodyParams,
headers: {'Content-Type': 'application/json'},
Expand Down Expand Up @@ -294,23 +294,23 @@ export class AssetsV1APIClient {

async get_collection(collection_name: NameType) {
return this.client.call({
path: `/atomicassets/v1/collections/${collection_name}`,
path: `/atomicassets/v1/collections/${pathSegment(collection_name)}`,
method: 'GET',
responseType: Assets.GetCollectionResponse,
})
}

async get_collection_stats(collection_name: NameType) {
return this.client.call({
path: `/atomicassets/v1/collections/${collection_name}/stats`,
path: `/atomicassets/v1/collections/${pathSegment(collection_name)}/stats`,
method: 'GET',
responseType: Assets.GetCollectionStatsResponse,
})
}

async get_collection_schemas(collection_name: NameType) {
return this.client.call({
path: `/atomicassets/v1/collections/${collection_name}/schemas`,
path: `/atomicassets/v1/collections/${pathSegment(collection_name)}/schemas`,
method: 'GET',
responseType: Assets.GetCollectionSchemasResponse,
})
Expand All @@ -329,7 +329,7 @@ export class AssetsV1APIClient {
const bodyParams = buildBodyParams(options)

return this.client.call({
path: `/atomicassets/v1/collections/${collection_name}/logs`,
path: `/atomicassets/v1/collections/${pathSegment(collection_name)}/logs`,
method: 'POST',
params: bodyParams,
headers: {'Content-Type': 'application/json'},
Expand Down Expand Up @@ -363,15 +363,19 @@ export class AssetsV1APIClient {

async get_schema(collection_name: NameType, schema_name: NameType) {
return this.client.call({
path: `/atomicassets/v1/schemas/${collection_name}/${schema_name}`,
path: `/atomicassets/v1/schemas/${pathSegment(collection_name)}/${pathSegment(
schema_name
)}`,
method: 'GET',
responseType: Assets.GetSchemaResponse,
})
}

async get_schema_stats(collection_name: NameType, schema_name: NameType) {
return this.client.call({
path: `/atomicassets/v1/schemas/${collection_name}/${schema_name}/stats`,
path: `/atomicassets/v1/schemas/${pathSegment(collection_name)}/${pathSegment(
schema_name
)}/stats`,
method: 'GET',
responseType: Assets.GetSchemaStatsResponse,
})
Expand All @@ -391,7 +395,9 @@ export class AssetsV1APIClient {
const bodyParams = buildBodyParams(options)

return this.client.call({
path: `/atomicassets/v1/schemas/${collection_name}/${schema_name}/logs`,
path: `/atomicassets/v1/schemas/${pathSegment(collection_name)}/${pathSegment(
schema_name
)}/logs`,
method: 'POST',
params: bodyParams,
headers: {'Content-Type': 'application/json'},
Expand Down Expand Up @@ -428,7 +434,9 @@ export class AssetsV1APIClient {

async get_template(collection_name: NameType, template_id: Int32Type) {
return this.client.call({
path: `/atomicassets/v1/templates/${collection_name}/${template_id}`,
path: `/atomicassets/v1/templates/${pathSegment(collection_name)}/${pathSegment(
template_id
)}`,
method: 'GET',
responseType: Assets.GetTemplateResponse,
})
Expand All @@ -442,8 +450,10 @@ export class AssetsV1APIClient {
) {
const path =
typeof template_id === 'undefined'
? `/atomicassets/v1/templates/${collection_name_or_template_id}/stats`
: `/atomicassets/v1/templates/${collection_name_or_template_id}/${template_id}/stats`
? `/atomicassets/v1/templates/${pathSegment(collection_name_or_template_id)}/stats`
: `/atomicassets/v1/templates/${pathSegment(
collection_name_or_template_id
)}/${pathSegment(template_id)}/stats`

return this.client.call({
path,
Expand All @@ -466,7 +476,9 @@ export class AssetsV1APIClient {
const bodyParams = buildBodyParams(options)

return this.client.call({
path: `/atomicassets/v1/templates/${collection_name}/${template_id}/logs`,
path: `/atomicassets/v1/templates/${pathSegment(collection_name)}/${pathSegment(
template_id
)}/logs`,
method: 'POST',
params: bodyParams,
headers: {'Content-Type': 'application/json'},
Expand Down Expand Up @@ -500,7 +512,7 @@ export class AssetsV1APIClient {

async get_offer(offer_id: UInt64Type) {
return this.client.call({
path: `/atomicassets/v1/offers/${offer_id}`,
path: `/atomicassets/v1/offers/${pathSegment(offer_id)}`,
method: 'GET',
responseType: Assets.GetOfferResponse,
})
Expand All @@ -519,7 +531,7 @@ export class AssetsV1APIClient {
const bodyParams = buildBodyParams(options)

return this.client.call({
path: `/atomicassets/v1/offers/${offer_id}/logs`,
path: `/atomicassets/v1/offers/${pathSegment(offer_id)}/logs`,
method: 'POST',
params: bodyParams,
headers: {'Content-Type': 'application/json'},
Expand Down Expand Up @@ -586,7 +598,7 @@ export class AssetsV1APIClient {
const bodyParams = buildBodyParams(options)

return this.client.call({
path: `/atomicassets/v1/accounts/${account}`,
path: `/atomicassets/v1/accounts/${pathSegment(account)}`,
method: 'POST',
params: bodyParams,
headers: {'Content-Type': 'application/json'},
Expand All @@ -596,7 +608,9 @@ export class AssetsV1APIClient {

async get_account_template_schema_count(account: NameType, collection_name: NameType) {
return this.client.call({
path: `/atomicassets/v1/accounts/${account}/${collection_name}`,
path: `/atomicassets/v1/accounts/${pathSegment(account)}/${pathSegment(
collection_name
)}`,
method: 'GET',
responseType: Assets.GetAccountTemplateSchemaCountResponse,
})
Expand Down Expand Up @@ -625,7 +639,7 @@ export class AssetsV1APIClient {
const bodyParams = buildBodyParams(options)

return this.client.call({
path: `/atomicassets/v1/burns/${account}`,
path: `/atomicassets/v1/burns/${pathSegment(account)}`,
method: 'POST',
params: bodyParams,
headers: {'Content-Type': 'application/json'},
Expand Down
61 changes: 61 additions & 0 deletions src/endpoints/market/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import {
Marketplace,
OfferObject,
ResponseStruct,
RoyaltyAccountTotal,
RoyaltyAttributeRule,
RoyaltyConfig,
RoyaltyPayout,
RoyaltyTemplateRule,
SaleObject,
SalePrice,
SalePriceDay,
Expand Down Expand Up @@ -103,6 +108,32 @@ export class MarketAccount extends Struct {
@Struct.field(AccountStat) declare result: AccountStat
}

@Struct.type('marketplace_stat')
export class MarketplaceStat extends Struct {
@Struct.field(Name) declare market_contract: Name
/**
* Null when no marketplace is stored, and the empty string when the
* stored value is the default marketplace name. Both mean the same
* thing to a client.
*/
@Struct.field('string', {optional: true}) declare marketplace_name: string
@Struct.field(UInt64) declare sellers: UInt64
@Struct.field(UInt64) declare buyers: UInt64
/**
* The field is declared optional defensively because the outer sort
* carries NULLS LAST. The aggregate answers `0`, not null, for a side
* with no rows.
*/
@Struct.field(UInt64, {optional: true}) declare maker_volume: UInt64
@Struct.field(UInt64, {optional: true}) declare taker_volume: UInt64
}

@Struct.type('market_marketplaces')
export class MarketMarketplaces extends Struct {
@Struct.field(Token) declare symbol: Token
@Struct.field(MarketplaceStat, {array: true}) declare results: MarketplaceStat[]
}

@Struct.type('schema_stat_v1')
export class SchemaStatV1 extends Struct {
@Struct.field(Name) declare contract: Name
Expand Down Expand Up @@ -303,3 +334,33 @@ export class GetStatsGraphResponse extends ResponseStruct {
export class GetStatsSalesResponse extends ResponseStruct {
@Struct.field(MarketSale) declare data: MarketSale
}

@Struct.type('get_stats_markets_resp')
export class GetStatsMarketsResponse extends ResponseStruct {
@Struct.field(MarketMarketplaces) declare data: MarketMarketplaces
}

@Struct.type('get_royalty_config_resp')
export class GetRoyaltyConfigResponse extends ResponseStruct {
@Struct.field(RoyaltyConfig) declare data: RoyaltyConfig
}

@Struct.type('get_royalty_template_rules_resp')
export class GetRoyaltyTemplateRulesResponse extends ResponseStruct {
@Struct.field(RoyaltyTemplateRule, {array: true}) declare data: RoyaltyTemplateRule[]
}

@Struct.type('get_royalty_attribute_rules_resp')
export class GetRoyaltyAttributeRulesResponse extends ResponseStruct {
@Struct.field(RoyaltyAttributeRule, {array: true}) declare data: RoyaltyAttributeRule[]
}

@Struct.type('get_royalty_payouts_resp')
export class GetRoyaltyPayoutsResponse extends ResponseStruct {
@Struct.field(RoyaltyPayout, {array: true}) declare data: RoyaltyPayout[]
}

@Struct.type('get_royalty_account_resp')
export class GetRoyaltyAccountResponse extends ResponseStruct {
@Struct.field(RoyaltyAccountTotal, {array: true}) declare data: RoyaltyAccountTotal[]
}
Loading
Loading