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.
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[]
}
152 changes: 152 additions & 0 deletions src/endpoints/market/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,26 @@ export interface GetTemplateBuyoffersOptions {
sort?: 'created' | 'updated' | 'ending' | 'buyoffer_id' | 'price' | 'template_mint' | 'name'
}

export interface GetRoyaltyPayoutsOptions {
recipient?: NameType[]
collection_name?: NameType[]
asset_id?: UInt64Type[]
symbol?: string
listing_type?: 'unresolved' | 'sale' | 'auction' | 'buyoffer' | 'template_buyoffer'
listing_id?: UInt64Type
category?: Array<'founders' | 'template' | 'attribute' | 'dust'>
ids?: UInt64Type[]
// lower_bound and upper_bound range over log_global_sequence.
lower_bound?: string
upper_bound?: string
before?: number
after?: number
page?: number
limit?: number
order?: 'asc' | 'desc'
sort?: 'created' | 'amount'
}

export class MarketV1APIClient {
constructor(private client: APIClient) {}

Expand Down Expand Up @@ -946,11 +966,143 @@ export class MarketV1APIClient {
})
}

/**
* Seller, buyer, and volume totals per marketplace for one token symbol.
*
* Bound the query with `collection_whitelist` or an `after` window. On a
* mainnet-sized indexer the unbounded aggregate exceeds the server timeout.
*/
async get_stats_markets(options: {
symbol: string
collection_blacklist?: NameType[]
collection_whitelist?: NameType[]
before?: number
after?: number
}) {
const bodyParams = buildBodyParams(options)

return this.client.call({
path: `/atomicmarket/v1/stats/markets`,
method: 'POST',
params: bodyParams,
headers: {'Content-Type': 'application/json'},
responseType: Market.GetStatsMarketsResponse,
})
}

async get_config() {
return this.client.call({
path: '/atomicmarket/v1/config',
method: 'GET',
responseType: Market.GetConfigResponse,
})
}

/**
* The royalty configuration of a collection, mirrored from `royaltyconf`.
*
* A collection with no configuration answers HTTP 416, which reaches the
* caller as an `APIError` with `error.response.status === 416`. That is the
* "not configured" answer rather than a failure, and every collection on an
* AtomicMarket v1 chain answers that way. An indexer that predates the
* royalty routes answers 404.
*/
async get_royalty_config(collection_name: NameType) {
return this.client.call({
path: `/atomicmarket/v1/royalties/${collection_name}`,
method: 'GET',
responseType: Market.GetRoyaltyConfigResponse,
})
}

async get_royalty_template_rules(
collection_name: NameType,
options?: {
template_id?: Int32Type[]
page?: number
limit?: number
}
) {
const bodyParams = buildBodyParams(options)

return this.client.call({
path: `/atomicmarket/v1/royalties/${collection_name}/templates`,
method: 'POST',
params: bodyParams,
headers: {'Content-Type': 'application/json'},
responseType: Market.GetRoyaltyTemplateRulesResponse,
})
}

async get_royalty_attribute_rules(
collection_name: NameType,
options?: {
source?: number
field?: string
page?: number
limit?: number
}
) {
const bodyParams = buildBodyParams(options)

return this.client.call({
path: `/atomicmarket/v1/royalties/${collection_name}/attributes`,
method: 'POST',
params: bodyParams,
headers: {'Content-Type': 'application/json'},
responseType: Market.GetRoyaltyAttributeRulesResponse,
})
}

/** The settled payout ledger. Empty on an AtomicMarket v1 chain. */
async get_royalty_payouts(
options?: GetRoyaltyPayoutsOptions,
extra_options?: {[key: string]: string}
) {
const bodyParams = buildBodyParams(options, extra_options)

return this.client.call({
path: `/atomicmarket/v1/royalties/payouts`,
method: 'POST',
params: bodyParams,
headers: {'Content-Type': 'application/json'},
responseType: Market.GetRoyaltyPayoutsResponse,
})
}

async get_royalty_payouts_count(
options?: GetRoyaltyPayoutsOptions,
extra_options?: {[key: string]: string}
) {
const bodyParams = buildBodyParams(options, extra_options)

return this.client.call({
path: `/atomicmarket/v1/royalties/payouts/_count`,
method: 'POST',
params: bodyParams,
headers: {'Content-Type': 'application/json'},
responseType: CountResponseStruct,
})
}

/** One row per token symbol the account has been paid in. */
async get_royalty_account(
account: NameType,
options?: {
collection_name?: NameType[]
symbol?: string
before?: number
after?: number
}
) {
const bodyParams = buildBodyParams(options)

return this.client.call({
path: `/atomicmarket/v1/royalties/accounts/${account}`,
method: 'POST',
params: bodyParams,
headers: {'Content-Type': 'application/json'},
responseType: Market.GetRoyaltyAccountResponse,
})
}
}
Loading
Loading