diff --git a/guides/asset-lifecycle.md b/guides/asset-lifecycle.md index 8fb2eca..ec29d4b 100644 --- a/guides/asset-lifecycle.md +++ b/guides/asset-lifecycle.md @@ -1,7 +1,10 @@ --- scope: Creator flow on the `atomicassets` contract - create a collection, define a schema, optionally a template, mint assets, edit mutable data, transfer, and burn depends-on: [reference/atomicassets/structure.md, reference/atomicassets/actions.md, reference/wharfkit.md] -key-modules: ["atomicmarket-contract (v2.0.0-rc2): src/atomicmarket.cpp", "atomicassets-contract (v2.0.0-rc4): src/atomicassets.cpp"] +key-modules: + - "atomicmarket-contract (v2.0.0-rc2): src/atomicmarket.cpp" + - "atomicassets-contract (v2.0.0-rc4): src/atomicassets.cpp" + - "@atomichub/atomicassets 2.1.1 (atomicassets-sdk v2.1.1, 5c70c62): src/Actions/Generator.ts" --- # Create a collection and mint assets @@ -201,6 +204,36 @@ Pass `template_id: -1` to mint a templateless asset carrying its own `immutable_ Source: `atomicassets-contract src/atomicassets.cpp:697-788` (`mintasset`, backing guard at `:786-787`), V1 backing behavior in this repo's V1 tree (`contracts/atomicassets-contract/src/atomicassets.cpp`) +### Building the same mint through the SDK + +`@atomichub/atomicassets` builds the identical action object and types the attribute map on the way in, so the eight positional arguments stay in ABI order and `createAttributeMap` picks the `ATOMIC_ATTRIBUTE` variant for each field: + +```ts +import { ActionBuilder, createAttributeMap } from '@atomichub/atomicassets' + +const builder = new ActionBuilder('atomicassets') +const mutable = createAttributeMap({ level: 1 }, { level: 'uint32' }) + +const mint = builder.mintasset( + session.actor.toString(), // authorized_minter + 'mycollectn1', // collection_name + 'cards', // schema_name + 123456, // template_id, -1 for a templateless asset + 'collector.wam', // new_asset_owner + [], // immutable_data + mutable, // mutable_data + [], // tokens_to_back +) + +await session.transact({ action: { ...mint, authorization: [session.permissionLevel] } }) +``` + +The builder returns `{ account, name, data }` and signs nothing, so the object above is the same payload the hand-written snippet sends. `tokens_to_back` is `[]` deliberately: the parameter carries a deprecation tag for the abort documented above. + +The numeric parameters are the one thing the builder checks, and it throws a `SerializationError` naming the offending field before any transaction is built. `template_id` is validated as an int32, which keeps `-1` available as the no-template sentinel and rejects a `NaN` that a string-to-number conversion produced; `max_supply` on `createtempl` is validated as a uint32, so a fractional or negative supply fails at the call rather than on chain. Without that check a `NaN` reaches the signing library as `null`, because JSON has no form for it, and the mistake is gone before the chain can name it. The full parameter list is in [@atomichub/atomicassets SDK](../reference/sdk/atomicassets.md#numeric-parameters-are-checked-against-their-abi-type-and-throw) ("Numeric parameters are checked against their ABI type and throw"). + +Source: atomicassets-sdk (v2.1.1, 5c70c62) src/Actions/Generator.ts:373-384 (`mintasset` and its `template_id` check), src/Actions/Generator.ts:126-156 (the numeric guards), src/Actions/Generator.ts:295-303 (`createtempl` `max_supply`), src/Actions/Generator.ts:358-372 (the `tokens_to_back` deprecation), src/Actions/Generator.ts:524-526 (`_action` returning one object) + ## Update mutable data: setassetdata ```json diff --git a/guides/auctions.md b/guides/auctions.md index 13cb133..8a03537 100644 --- a/guides/auctions.md +++ b/guides/auctions.md @@ -1,7 +1,10 @@ --- scope: AtomicMarket auction lifecycle on the V2 baseline - announce, transfer the asset into escrow, place deposit-backed bids, claim after the end, and cancel depends-on: [reference/atomicmarket/actions.md, guides/deposits.md] -key-modules: ["atomicmarket-contract (v2.0.0-rc2): src/atomicmarket.cpp", "atomicassets-contract (v2.0.0-rc4): src/atomicassets.cpp"] +key-modules: + - "atomicmarket-contract (v2.0.0-rc2): src/atomicmarket.cpp" + - "atomicassets-contract (v2.0.0-rc4): src/atomicassets.cpp" + - "@atomichub/atomicmarket 2.4.1 (atomicmarket-sdk v2.4.1, 437300b): src/Actions/Generator.ts" --- # Working with auctions @@ -104,6 +107,34 @@ Failure modes asserted in source: Source: `atomicmarket-contract src/atomicmarket.cpp:1889-1943` (`receive_asset_transfer`) +### Building the announce and escrow pair with the SDK + +The order above is the contract's, not a preference: the transfer's notification handler looks up an announced auction by its assets and seller and aborts when it finds none, so a transfer that arrives first fails. `@atomichub/atomicmarket` composes the pair in that order, with the `auction` memo literal filled in: + +```ts +import { MarketActionBuilder } from '@atomichub/atomicmarket' + +const builder = new MarketActionBuilder('atomicmarket') + +const actions = builder.announceAuctionActions({ + seller: session.actor.toString(), + asset_ids: ['1099511627887'], + starting_bid: '10.00000000 WAX', + duration: 86400, + maker_marketplace: 'mymarket', + assets_contract: 'atomicassets', +}) +// -> [announceauct on atomicmarket, transfer on atomicassets with memo 'auction'] + +await session.transact({ + actions: actions.map((a) => ({ ...a, authorization: [session.permissionLevel] })), +}) +``` + +`duration` is the one field the builder checks: it must be a whole number inside the uint32 range, or `announceauct` throws before a transaction is built. That is a serialization bound rather than a chain rule, so the config's minimum and maximum auction duration still apply and are still the chain's to enforce. Nothing else is checked, and the composer carries no bundle opt-out, because an auction action is handed an auction id and cannot see how many assets the row holds. See [@atomichub/atomicmarket SDK](../reference/sdk/atomicmarket.md#the-five-composers) ("The five composers"). + +Source: atomicmarket-sdk (v2.4.1, 437300b) src/Actions/Generator.ts:615-639 (`announceAuctionActions`, the ordering rule and the `auction` memo), src/Actions/Generator.ts:312-323 (`announceauct` and its `duration` check), src/Actions/Generator.ts:713-729 (`_uint32`), src/Actions/Generator.ts:301-311 (the legacy bundle note on the auction family) + ## Bid on an auction ```json diff --git a/guides/buyoffers.md b/guides/buyoffers.md index 256abba..35e526f 100644 --- a/guides/buyoffers.md +++ b/guides/buyoffers.md @@ -1,7 +1,10 @@ --- scope: How to create, accept, decline, and cancel AtomicMarket asset and template buyoffers, whose price leaves the buyer's deposited balance at creation depends-on: [reference/atomicmarket/actions.md, guides/deposits.md, reference/api.md] -key-modules: ["atomicmarket-contract (v2.0.0-rc2): src/atomicmarket.cpp", "atomicassets-contract (v2.0.0-rc4): src/atomicassets.cpp"] +key-modules: + - "atomicmarket-contract (v2.0.0-rc2): src/atomicmarket.cpp" + - "atomicassets-contract (v2.0.0-rc4): src/atomicassets.cpp" + - "@atomichub/atomicmarket 2.4.1 (atomicmarket-sdk v2.4.1, 437300b): src/Actions/Generator.ts" --- # Buyoffers @@ -98,6 +101,36 @@ Changed in V2: see [AtomicMarket V2 changes](../reference/atomicmarket/v2-change Source: `atomicmarket-contract src/atomicmarket.cpp:1533-1626` (`acceptbuyo`), `atomicmarket-contract include/atomicmarket.hpp:260-265` +#### Building the accept flow with the SDK + +Because `acceptbuyo` identifies its offer as the globally last created row rather than by an id, an `acceptbuyo` action built on its own is not safe to send. `@atomichub/atomicmarket` gives it no standalone builder method for that reason; the only way to reach it is `acceptBuyofferActions`, which emits the `createoffer` and the `acceptbuyo` together, in that order, with the `buyoffer` memo filled in: + +```ts +import { MarketActionBuilder } from '@atomichub/atomicmarket' + +const builder = new MarketActionBuilder('atomicmarket') + +const actions = builder.acceptBuyofferActions({ + recipient: session.actor.toString(), + buyoffer_id: '42', + asset_ids: ['1099511627776'], + expected_price: '10.00000000 WAX', + taker_marketplace: 'atomichub', + assets_contract: 'atomicassets', +}) +// -> [createoffer on atomicassets with memo 'buyoffer', acceptbuyo on atomicmarket] + +await session.transact({ + actions: actions.map((a) => ({ ...a, authorization: [session.permissionLevel] })), +}) +``` + +The composer fills `expected_asset_ids` from `asset_ids`, because the contract compares that list twice: once against the buyoffer row and once against the contents of the offer it reads. It does not accept the offer itself, since the market contract sends that `acceptoffer` inline and a pre-accepted offer is gone from the table before the contract can find it. Nothing else in the transaction may create an AtomicAssets offer between these two actions; actions appended after `acceptbuyo` are safe. + +The composer throws when `asset_ids` carries more than one id, unless `allow_v1_bundle_buyoffer: true` is set. Under V2 `acceptbuyo` refunds the escrowed price and erases a multi-asset row before it ever reads the offers table, so the transaction commits with the buyoffer gone, nothing sold, and the offer this flow created left dangling on the recipient's RAM until they cancel it. Set the flag only against a chain still running AtomicMarket V1, where bundle buyoffers accept correctly. See [@atomichub/atomicmarket SDK](../reference/sdk/atomicmarket.md#the-two-bundle-opt-out-flags) ("The two bundle opt-out flags"). + +Source: atomicmarket-sdk (v2.4.1, 437300b) src/Actions/Generator.ts:641-688 (`acceptBuyofferActions`, the last-offer placement rule, the bundle throw), src/Actions/Generator.ts:158-175 (`AcceptBuyofferInput` and `allow_v1_bundle_buyoffer`), src/Actions/Generator.ts:208-476 (the builder's action set, which carries no standalone `acceptbuyo`) + ### Declining a buyoffer `declinebuyo` requires the recipient's authorization (not the buyer's) and refunds the escrowed price to the buyer's deposited balance. The buyer must then `withdraw` it, since nothing is transferred out automatically. @@ -248,6 +281,26 @@ await session.transact({ Source: `atomicmarket-contract src/atomicmarket.cpp:1717-1794` (`fulfilltbuyo`), `atomicmarket-contract include/atomicmarket.hpp:308-314` +#### Building the fulfill flow with the SDK + +`fulfilltbuyo` reads the last offer the same way `acceptbuyo` does, so it has no standalone builder method either. `fulfillTemplateBuyofferActions` emits the pair with the `tbuyoffer` memo filled in: + +```ts +const actions = builder.fulfillTemplateBuyofferActions({ + seller: session.actor.toString(), + buyoffer_id: '7', + asset_id: '1099511627777', + expected_price: '5.00000000 WAX', + taker_marketplace: 'atomichub', + assets_contract: 'atomicassets', +}) +// -> [createoffer on atomicassets with memo 'tbuyoffer', fulfilltbuyo on atomicmarket] +``` + +It carries no bundle guard, because a template buyoffer names one asset by construction: `fulfilltbuyo` takes a single `asset_id` and the contract checks that the offer holds exactly that one asset. The placement rule from the accept flow above applies unchanged, and it is the SDK-side statement of the marketplace security consideration in this section: keep the offer immediately before the market action and let nothing else create an offer in between. + +Source: atomicmarket-sdk (v2.4.1, 437300b) src/Actions/Generator.ts:690-707 (`fulfillTemplateBuyofferActions` and why it carries no bundle guard), src/Actions/Generator.ts:177-188 (`FulfillTemplateBuyofferInput`), src/Actions/Generator.ts:641-657 (the last-offer placement rule shared with the accept flow) + ### Cancelling a template buyoffer `canceltbuyo` requires the buyer's authorization and refunds the escrowed price to the buyer's deposited balance, the same as `cancelbuyo`. diff --git a/guides/querying-the-api.md b/guides/querying-the-api.md index 0854e80..93d6293 100644 --- a/guides/querying-the-api.md +++ b/guides/querying-the-api.md @@ -10,7 +10,35 @@ Workflow patterns for reading Atomic data, combining facts from the `reference/` For the full endpoint, parameter, and schema listing, use the deployment's Swagger UI (`https://wax.api.atomicassets.io/docs/` on the WAX reference deployment); see [atomicassets-api HTTP API](../reference/api.md#interactive-reference-swagger-ui) ("Interactive reference (Swagger UI)"), which also covers why no standalone OpenAPI JSON is published. -The examples here use WAX mainnet hosts. The `atomicassets` and `atomicmarket` contract accounts have the same names on WAX testnet, but switching chains means swapping two hosts, not one. Point chain reads (`get_table_rows`) at a testnet node such as `https://waxtestnet.greymass.com`, and point HTTP API reads at the testnet reference deployment `https://test.wax.api.atomicassets.io` (the same atomicassets-api software indexing the testnet chain). The mainnet API host `wax.api.atomicassets.io` has no testnet data, so a testnet integrator that changes only the RPC node and keeps the mainnet API host reads an unrelated chain. +## Switching to testnet means swapping two hosts, not one + +The examples here use WAX mainnet hosts. The `atomicassets` and `atomicmarket` contract accounts have the same names on WAX testnet, so nothing in an action's data changes, and that is exactly what makes the mistake easy to miss. + +| Read | WAX mainnet | WAX testnet | +| --- | --- | --- | +| Chain tables (`get_table_rows`) | `https://wax.greymass.com` | `https://waxtestnet.greymass.com` | +| HTTP API | `https://wax.api.atomicassets.io` | `https://test.wax.api.atomicassets.io` | + +The testnet API host runs the same atomicassets-api software indexing the testnet chain. The mainnet host has no testnet data, so an integrator who changes only the RPC node and keeps the mainnet API host reads an unrelated chain and sees a working request return facts about someone else's assets. Testnet is also where the V2 contracts run, so a V2-only route such as the royalty read layer answers there and returns HTTP 416 everywhere on mainnet; see [atomicassets-api HTTP API](../reference/api.md#the-royalty-routes-answer-416-when-a-collection-has-no-config) ("The royalty routes answer 416 when a collection has no config"). + +## Percent-encode every caller-supplied URL part + +Both SDKs percent-encode caller-supplied path segments and both the key and the value of every query parameter. A hand-rolled URL has to do the same. An asset id, collection, schema, template, or account name that carries `/`, `?`, or `#` escapes its own path segment and sends the request somewhere else; a data-filter key carrying `&` or `=` appends query parameters of its own. + +``` +// correct: encodeURIComponent (or the equivalent) on each path segment, and on both sides of every query pair +// avoid: pasting a caller-supplied value straight into the URL string +``` + +Encoding the whole key is safe even where it looks unnecessary. The typed data filters carry a colon (`data:number.level`, `data:bool.foil`, `data:text.rarity`), which encodes to `data%3Anumber.level` on the wire, and the deployment answers both spellings identically: + +```sh +curl 'https://wax.api.atomicassets.io/atomicassets/v1/templates?collection_name=alien.worlds&data:text.rarity=Common&limit=2' +curl 'https://wax.api.atomicassets.io/atomicassets/v1/templates?collection_name=alien.worlds&data%3Atext.rarity=Common&limit=2' +# both return the same two templates (906463, 906461) +``` + +See [@atomichub/atomicassets SDK](../reference/sdk/atomicassets.md#path-segments-and-query-keys-are-percent-encoded) ("Path segments and query keys are percent-encoded") for what the SDKs do on the caller's behalf. ## Paginate list endpoints under the limit cap diff --git a/guides/sales.md b/guides/sales.md index 3e017d7..3ec71e4 100644 --- a/guides/sales.md +++ b/guides/sales.md @@ -1,7 +1,10 @@ --- scope: AtomicMarket instant-sale lifecycle - announce, escrow through an AtomicAssets offer, purchase, cancel, and Delphi Oracle sales that settle in another token depends-on: [reference/atomicmarket/actions.md, reference/atomicmarket/fees-and-royalties.md, guides/offers.md] -key-modules: ["atomicmarket-contract (v2.0.0-rc2): src/atomicmarket.cpp", "atomicassets-contract (v2.0.0-rc4): src/atomicassets.cpp"] +key-modules: + - "atomicmarket-contract (v2.0.0-rc2): src/atomicmarket.cpp" + - "atomicassets-contract (v2.0.0-rc4): src/atomicassets.cpp" + - "@atomichub/atomicmarket 2.4.1 (atomicmarket-sdk v2.4.1, 437300b): src/Actions/Generator.ts, src/Actions/Delphi.ts" --- # Working with sales @@ -106,6 +109,32 @@ Failure modes asserted in source: Source: `atomicmarket-contract src/atomicmarket.cpp:1950-2013` (`receive_asset_offer`) +### Building the listing pair with the SDK + +Announcing alone lists nothing and offering alone dangles, so the two actions belong in one transaction. `@atomichub/atomicmarket` composes that pair, filling in the `sale` memo literal and the AtomicAssets contract account: + +```ts +import { MarketActionBuilder } from '@atomichub/atomicmarket' + +const builder = new MarketActionBuilder('atomicmarket') + +const actions = builder.announceSaleActions({ + seller: session.actor.toString(), + asset_ids: ['1099511627887'], + listing_price: '100.00000000 WAX', + settlement_symbol: '8,WAX', + maker_marketplace: 'mymarket', + assets_contract: 'atomicassets', +}) +// -> [announcesale on atomicmarket, createoffer on atomicassets with memo 'sale'] + +await session.transact({ + actions: actions.map((a) => ({ ...a, authorization: [session.permissionLevel] })), +}) +``` + +The composer checks nothing. Whether the settlement symbol is a supported token, whether the listing and settlement symbols are a registered pair, and whether the marketplace exists are all chain state, and each refusal is a rejected transaction rather than a silent loss. See [@atomichub/atomicmarket SDK](../reference/sdk/atomicmarket.md#the-five-composers) ("The five composers"). + ## Purchase a sale ```json @@ -163,6 +192,30 @@ Optionally guard against the sale changing between when a buyer reads it and whe Source: `atomicmarket-contract src/atomicmarket.cpp:896-1015` (`purchasesale`, `assertsale`), `atomicmarket-contract src/atomicmarket.cpp:2468-2515` (`calc_settlement_price`) +### Building the purchase triple with the SDK + +A purchase is three actions: assert the terms, deposit the settlement quantity into the market contract's balance, then purchase against that balance. The deposit transfer belongs to the settlement token's own contract, not to AtomicMarket, and carries the memo `deposit`. `purchaseSaleActions` composes all three: + +```ts +const actions = builder.purchaseSaleActions({ + buyer: session.actor.toString(), + sale_id: '7', + asset_ids: ['1099511627887'], + listing_price: '100.00000000 WAX', + settlement_symbol: '8,WAX', + intended_delphi_median: '0', + token_contract: 'eosio.token', + taker_marketplace: 'othermarket', +}) +// -> [assertsale, transfer on eosio.token with memo 'deposit', purchasesale] +``` + +Only the purchase's place in that order is fixed. It spends the deposited balance and erases the sale row `assertsale` reads, so both of the others must precede it; the assert and the deposit may swap. + +The composer throws when `asset_ids` carries more than one id, unless `allow_v1_bundle_sale: true` is set. That guard exists because the bundle case is the one caller error the chain neither reverts nor refuses: under V2 `purchasesale` returns early for a multi-asset row, declining the offer and erasing it before touching any balance, while `assertsale` has already passed and the deposit has already credited the buyer. The transaction commits with the buyer paid, nothing delivered, and the tokens recoverable only through a separate `withdraw`. Set the flag only against a chain still running AtomicMarket V1, where bundles are ordinary listings. See [@atomichub/atomicmarket SDK](../reference/sdk/atomicmarket.md#the-two-bundle-opt-out-flags) ("The two bundle opt-out flags"). + +Source: `atomicmarket-contract src/atomicmarket.cpp:896-1015` (`purchasesale` early return on a multi-asset row); atomicmarket-sdk (v2.4.1, 437300b) src/Actions/Generator.ts:493-592 (`purchaseSaleActions`, the emitted order, the bundle throw), src/Actions/Generator.ts:578-591 (the `deposit` memo and the settlement token's own contract) + ## Cancel a sale ```json @@ -242,3 +295,18 @@ curl -X POST https://wax.greymass.com/v1/chain/get_table_rows \ ``` Source: `atomicmarket-contract src/atomicmarket.cpp:2468-2515` (`calc_settlement_price`) + +### What settlement_quantity has to be + +`purchasesale` spends the buyer's AtomicMarket balance, so the buyer funds it with a deposit transfer in the same transaction. The size and symbol of that transfer are `settlement_quantity`, and nothing on chain checks it: `assertsale` pins the listing terms and says nothing about the deposit. Two rules follow, and an integrator meets both before anything else about Delphi pricing matters. + +| The sale | What the deposit has to be | `intended_delphi_median` | +| --- | --- | --- | +| `settlement_symbol` differs from the listing price's own symbol | `settlement_quantity` is required, and it is denominated in the settlement symbol | the exact median the purchase asserts | +| The two name one symbol | `settlement_quantity` may be omitted; a supplied one equals `listing_price` exactly | `0` | + +A cross-symbol sale settles the oracle conversion of its listing price, so reusing the listing price as the deposit sends an amount in the wrong symbol. A short deposit draws the difference from whatever balance the buyer already holds, and a wrong-symbol deposit credits a balance the purchase never spends. The whole symbol decides which row applies, precision and code both, so a sale listing `30.00 WAX` against `8,WAX` names two symbols and settles through the oracle like any other cross-symbol sale. + +`@atomichub/atomicmarket` enforces both rules in `purchaseSaleActions` and derives the amount for the first one: `deriveSettlementAmount(listingAmount, median, pair)` reproduces the contract's own conversion, and `formatQuantity` renders it as the quantity string the transfer needs. Reproducing the arithmetic by hand is the step to skip; deriving the exact rational floor instead of the contract's truncated double leaves the deposit a raw unit short and the purchase throws. See [@atomichub/atomicmarket SDK](../reference/sdk/atomicmarket.md#delphi-settlement-math-derivesettlementamount-and-formatquantity) ("Delphi settlement math"). + +Source: `atomicmarket-contract src/atomicmarket.cpp:2468-2515` (`calc_settlement_price`, the same-symbol branch and the oracle branch); atomicmarket-sdk (v2.4.1, 437300b) src/Actions/Generator.ts:106-112 (the `settlement_quantity` contract), src/Actions/Generator.ts:522-576 (both branches enforced), src/Actions/Delphi.ts:72-122 (`deriveSettlementAmount` and the truncation it reproduces) diff --git a/learning/INSTRUCTIONS.md b/learning/INSTRUCTIONS.md index 5ce2b85..a396bb5 100644 --- a/learning/INSTRUCTIONS.md +++ b/learning/INSTRUCTIONS.md @@ -42,4 +42,10 @@ Promotion means: the claim is checked against source or a live read, the result ## Current state -This log starts empty. Everything shipped in `reference/` and `guides/` at repository creation was already validated before it landed there, so there is nothing pending promotion yet. New entries arrive as work on drops, packs, EVM chains, or any other unvalidated claim begins. +Everything in `reference/` and `guides/` was validated before it landed there, so no polished-tier fact is waiting on this log. What sits here instead is the residue of writing that tier: a claim that came up while documenting a validated surface, whose own check needs a source read or a chain state nobody has yet. + +| File | Holds | +| --- | --- | +| `api.md` | Claims about the atomicassets-api hosted HTTP surface | + +New files arrive as work on drops, packs, EVM chains, or any other unvalidated area begins, one per polished-tier area. diff --git a/learning/api.md b/learning/api.md new file mode 100644 index 0000000..c2d02bd --- /dev/null +++ b/learning/api.md @@ -0,0 +1,17 @@ +--- +scope: Unvalidated claims about the atomicassets-api hosted HTTP surface, waiting on a source read or a live probe before they can enter the polished tier +depends-on: [] +key-modules: [] +--- + +# Learning log: hosted HTTP API + +Unvalidated claims about the atomicassets-api HTTP surface. See `INSTRUCTIONS.md` for the entry format and the promotion gate. + +## The /v2/sales route omits sales in the Waiting state + +- **Claim.** `/atomicmarket/v2/sales` excludes sales whose `state` is `0` (WAITING, announced but with no escrow offer yet), so a query that wants those rows belongs on `/atomicmarket/v1/sales`. +- **How it would be validated.** Read the route's query construction in the atomicassets-api source (`src/api/namespaces/atomicmarket/routes/sales.ts` and the materialized-view definition the `/v2` handler selects from) at a pinned commit, and check whether the view's predicate filters the waiting state. A live probe cannot settle it alone: a waiting sale exists only between `announcesale` and the escrow offer, and both the WAX mainnet and the WAX testnet deployments report a `state=0` count of zero on `/v1` and `/v2` alike, so the two routes agree vacuously. A live confirmation needs a sale announced without an offer on a chain the prober controls, then the same `state=0` query against both routes. +- **Promote to:** `reference/api.md`, the section "Two sales list routes answer on the hosted deployment". + +What is already validated and lives in `reference/api.md`: both routes answer 200 on the reference deployment, their unfiltered and `state=1` counts match, the row shapes are the same (both are typed `ISale` in `@atomichub/atomicmarket`), and the served OpenAPI document describes `/v2/sales` and not `/v1/sales`. Only the waiting-state exclusion is unvalidated, which is why the polished page states the routes without it. diff --git a/reference/api.md b/reference/api.md index ebc4ecd..da22a21 100644 --- a/reference/api.md +++ b/reference/api.md @@ -1,5 +1,5 @@ --- -scope: atomicassets-api HTTP API behavior - Swagger reference, the 100-row list cap, template buyoffer lifecycle states, per-endpoint state values, and rate limits +scope: atomicassets-api HTTP API behavior - Swagger reference, the 100-row list cap, both sales routes, royalty 416s, buyoffer lifecycle states, and rate limits depends-on: [] key-modules: - "atomicassets-api (main): src/api/server.ts, src/api/namespaces/*/openapi.ts" @@ -19,6 +19,20 @@ Source: `atomicassets-api src/api/server.ts` (`swagger.setup` mounted at `/docs` The atomicassets-api validates the `limit` query parameter on list endpoints such as `/atomicmarket/v1/buyoffers` and `/atomicmarket/v1/sales` against a maximum that defaults to 100; requests above the cap are rejected with HTTP 400 and `{"success": false, "message": "Invalid value for parameter limit"}` rather than being clamped. The cap is an operator-configurable server setting (`limits` in the API config), so the reference deployment at wax.api.atomicassets.io enforces 100. Pagination code must therefore bound `limit` to 100 and use `page`, and counting code must treat a non-2xx response as an error: an HTTP client helper that returns undefined or empty on failure will silently turn an over-limit request into a zero count. +## Two sales list routes answer on the hosted deployment + +The reference deployment serves both `/atomicmarket/v1/sales` and `/atomicmarket/v2/sales`. The `/v2` route is the newer materialized sales index; it returns the same row shape as `/v1`, takes the same filters and the same `state` values, and carries the same `/_count` sibling. Which of the two the OpenAPI document describes is the surprise: the served document lists `/atomicmarket/v2/sales` and does not list `/atomicmarket/v1/sales` at all, although `/v1/sales` answers 200. A code generator run against the spec therefore emits the `/v2` route only, while every existing integration and every example in this repository calls `/v1`. Both are live; pick one deliberately rather than by whichever the tooling surfaced. In `@atomichub/atomicmarket` the pair is `getSales`/`countSales` against `/v1` and `getSalesV2`/`countSalesV2` against `/v2` (see [@atomichub/atomicmarket SDK](sdk/atomicmarket.md)). + +Source: live probes of `https://wax.api.atomicassets.io/atomicmarket/v1/sales?limit=1` (200), `/atomicmarket/v2/sales?limit=1` (200), and `/_count` on both unfiltered and at `state=1` (200, equal counts each time), plus the OpenAPI document embedded in `https://wax.api.atomicassets.io/docs/swagger-ui-init.js` (carries `/atomicmarket/v2/sales`, carries no `/atomicmarket/v1/sales`); atomicmarket-sdk (v2.4.1, 437300b) src/API/Explorer/index.ts:76-100 (both routes typed `ISale` and taking `SaleApiParams`) + +## The royalty routes answer 416 when a collection has no config + +The AtomicMarket v2 royalty routes (`/atomicmarket/v1/royalties/{collection_name}` and its `/templates` and `/attributes` children) answer HTTP 416 with `{"success": false, "message": "Royalty config not found"}` for a collection that has no royalty configuration. That is the normal empty result, not a transport or a routing failure, and 404 is not what these routes return. + +The distinction matters on WAX mainnet, which still runs the V1 contracts and therefore has no royalty configuration for any collection: every mainnet request to these routes answers 416. A client that treats 416 as "this collection configured no royalties" reads a chain-wide "the route has no data here" as a per-collection fact. `@atomichub/atomicmarket`'s `getRoyaltyConfig` maps 416 to `null` and lets every other status raise, so the same trap sits behind a `null` there; see [@atomichub/atomicmarket SDK](sdk/atomicmarket.md). + +Source: live probes of `https://wax.api.atomicassets.io/atomicmarket/v1/royalties/pixeltycoons` (416, `Royalty config not found`), `https://test.wax.api.atomicassets.io/atomicmarket/v1/royalties/royaltycol11` (200) and `/royalties/farmmetricsx` (416); the five royalty routes are listed in the OpenAPI document at `https://wax.api.atomicassets.io/docs/swagger-ui-init.js` + ## Template buyoffers keep all lifecycle states AtomicMarket template buyoffers in the atomicassets-api follow a three-state lifecycle: `lognewtbuyo` inserts a row in state 0 (LISTED), `canceltbuyo` flips it to 1 (CANCELED), and `fulfilltbuyo` flips it to 2 (SOLD), setting the seller and inserting the fulfilled asset rows. Rows are never deleted or archived: no maintenance job cleans up CANCELED or SOLD offers, so they persist indefinitely as state markers. The `/v1/template_buyoffers` endpoint applies no state filter by default: without an explicit `state` query parameter it returns offers in all three states, so clients that only want active offers must pass `state=0`. No socket notifications are broadcast for template buyoffers at the pinned commit: the socket handler for new offers exists in the source but is never wired into the `atomicmarket` namespace, and cancellation and fulfillment have no handler at all (`reference/api-streaming.md`). Poll the endpoint rather than waiting on socket events. The filler and API state enums both encode LISTED=0, CANCELED=1, SOLD=2 and map 1:1. diff --git a/reference/sdk/atomicassets.md b/reference/sdk/atomicassets.md index 2c63d3d..4f6e6b9 100644 --- a/reference/sdk/atomicassets.md +++ b/reference/sdk/atomicassets.md @@ -2,12 +2,12 @@ scope: "@atomichub/atomicassets JavaScript/TypeScript SDK: ExplorerApi and RpcApi reads, attribute serialization, v2 action building, and the network factories" depends-on: [reference/api.md, reference/wharfkit.md, reference/atomicassets/serialization.md] key-modules: - - "@atomichub/atomicassets 2.0.0 (atomicassets-sdk main, 80580c5): src/index.ts, src/API/Explorer/index.ts, src/API/Rpc/index.ts, src/Actions/Generator.ts, src/Serialization/index.ts, src/Schema/index.ts, src/Networks.ts" + - "@atomichub/atomicassets 2.1.1 (atomicassets-sdk v2.1.1, 5c70c62): src/index.ts, src/API/Explorer/index.ts, src/API/Rpc/index.ts, src/Actions/Generator.ts, src/Serialization/index.ts, src/Schema/index.ts, src/Networks.ts" --- # @atomichub/atomicassets SDK -The official JavaScript/TypeScript client for the AtomicAssets standard on Antelope chains. It reads asset data over the hosted API and directly from chain tables, serializes and deserializes attribute data, and builds v2 contract actions for a signer to sign. Version-sensitive facts below were read from the 2.0.0 source tree; re-verify against current source after an upgrade. +The official JavaScript/TypeScript client for the AtomicAssets standard on Antelope chains. It reads asset data over the hosted API and directly from chain tables, serializes and deserializes attribute data, and builds v2 contract actions for a signer to sign. Version-sensitive facts below were read from the 2.1.1 source tree; re-verify against current source after an upgrade. ``` npm install @atomichub/atomicassets @@ -15,19 +15,88 @@ npm install @atomichub/atomicassets ## The package has zero runtime dependencies and ships ESM and CJS -`@atomichub/atomicassets` declares no runtime `dependencies`; everything it needs (fetch, serialization, the queue) is either built in or supplied by the host runtime's global `fetch`. It publishes dual builds (`build/index.mjs` for `import`, `build/index.cjs` for `require`) with types for both, and requires Node `>=20`. Every public type and value is re-exported from the package root, so consumers import from `@atomichub/atomicassets` and never reach into `build/` subpaths. +`@atomichub/atomicassets` declares no runtime `dependencies`; everything it needs (fetch, serialization, the queue) is either built in or supplied by the host runtime's global `fetch`. It publishes dual builds (`build/index.mjs` for `import`, `build/index.cjs` for `require`) with types for both, and requires Node `>=20`. The package declares `sideEffects: false`, so a bundler may drop what an application does not import: importing only `ActionBuilder` no longer pulls in the base58 coder, the parser table, or the action-name map. Every public type and value is re-exported from the package root, so consumers import from `@atomichub/atomicassets` and never reach into `build/` subpaths. -Source: atomicassets-sdk (main, 80580c5) package.json (no `dependencies` key; `main`/`module`/`exports` dual build; `engines.node >=20`), src/index.ts (flat root re-exports) +Source: atomicassets-sdk (v2.1.1, 5c70c62) package.json:32 (`sideEffects: false`), package.json:34-35 (`engines.node >=20`), package.json (no `dependencies` key; `main`/`module`/`exports` dual build), src/index.ts:10-61 (flat root re-exports) ## ExplorerApi reads the hosted atomicassets-api -`ExplorerApi` wraps the hosted HTTP API (the same endpoints documented in `reference/api.md`). The constructor takes `(endpoint, namespace, { fetch? })`: `endpoint` is the deployment host (`https://wax.api.atomicassets.io`), `namespace` is the API namespace (`atomicassets`), and the optional `fetch` overrides the runtime global (bound to `globalThis` by default, because a browser `fetch` called bare throws "Illegal invocation"). Constructing an `ExplorerApi` eagerly fires one `/v1/config` request: the instance exposes an `action` promise that resolves to an `ExplorerActionGenerator` bound to the config's contract account. +`ExplorerApi` wraps the hosted HTTP API (the same endpoints documented in [atomicassets-api HTTP API](../api.md)). The constructor takes `(endpoint, namespace, { fetch? })`: `endpoint` is the deployment host (`https://wax.api.atomicassets.io`), `namespace` is the API namespace (`atomicassets`), and the optional `fetch` overrides the runtime global (bound to `globalThis` by default, because a browser `fetch` called bare throws "Illegal invocation"). -The getters map one-to-one onto API routes and return the response `data` payload already unwrapped: `getAsset(id)`, `getAssets(options, page, limit, data)`, `getTemplates(options, page, limit, data)`, `getCollections`, `getSchemas`, `getOffers`, `getTransfers`, `getAccounts`, plus per-entity `getX`, `getXStats`, `getXLogs`, and `countX` variants. List getters default to `page = 1`, `limit = 100`. Options are typed per entity (`AssetsApiParams`, `TemplateApiParams`, `CollectionApiParams`, and so on), each carrying the filter, greylist, boundary, `sort`, and `order` fields that route accepts; `sort` and `order` values are the `AssetsSort`/`OrderParam` string enums exported from the root. +Construction starts no network request. `explorerApi.action` is a read-only getter returning a promise of an `ExplorerActionGenerator` bound to the contract account in `/v1/config`; that config is fetched on first access, shared between concurrent accessors, and refetched on the next access after a failure. Because it is a getter it does not appear in `Object.keys` or a spread of the instance, and assigning to it throws. -The final `data` argument on `getAssets`/`getTemplates` targets the on-chain data filters: each entry `{ key, value, type? }` becomes a query field keyed `data.`, `data:number.`, or `data:bool.` by the value's JS type (`type` defaults to `data`, and can be set to `template_data`/`immutable_data`/`mutable_data`). Requests whose query string reaches 1000 characters are sent as a POST with a JSON body instead of a GET, transparently to the caller. +Source: atomicassets-sdk (v2.1.1, 5c70c62) src/API/Explorer/index.ts:84-89 (no constructor I/O, cached and cleared on failure), src/API/Explorer/index.ts:96-107 (constructor, bound fetch), src/API/Explorer/index.ts:109-128 (`action` getter) -Live reads against `https://wax.api.atomicassets.io` (WAX mainnet): +### The getter surface + +Every getter maps onto one API route and returns the response `data` payload already unwrapped. List getters default to `page = 1` and `limit = 100`. + +| Method | Route | Returns | +| --- | --- | --- | +| `getConfig()` | `/v1/config` | `IConfig` | +| `getAssets(options, page, limit, data)` | `/v1/assets` | `IAsset[]` | +| `countAssets(options, data)` | `/v1/assets/_count` | `number` | +| `getAsset(id)` | `/v1/assets/{id}` | `IAsset` | +| `getAssetStats(id)` | `/v1/assets/{id}/stats` | `IAssetStats` | +| `getAssetLogs(id, page, limit, order)` | `/v1/assets/{id}/logs` | `ILog[]` | +| `getCollections(options, page, limit)` | `/v1/collections` | `ICollection[]` | +| `countCollections(options)` | `/v1/collections/_count` | `number` | +| `getCollection(name)` | `/v1/collections/{name}` | `ICollection` | +| `getCollectionStats(name)` | `/v1/collections/{name}/stats` | `ICollectionStats` | +| `getCollectionLogs(name, page, limit, order)` | `/v1/collections/{name}/logs` | `ILog[]` | +| `getSchemas(options, page, limit)` | `/v1/schemas` | `IApiSchema[]` | +| `countSchemas(options)` | `/v1/schemas/_count` | `number` | +| `getSchema(collection, name)` | `/v1/schemas/{collection}/{name}` | `IApiSchema` | +| `getSchemaStats(collection, name)` | `/v1/schemas/{collection}/{name}/stats` | `ISchemaStats` | +| `getSchemaLogs(collection, name, page, limit, order)` | `/v1/schemas/{collection}/{name}/logs` | `ILog[]` | +| `getTemplates(options, page, limit, data)` | `/v1/templates` | `ITemplate[]` | +| `countTemplates(options, data)` | `/v1/templates/_count` | `number` | +| `getTemplate(collection, id)` | `/v1/templates/{collection}/{id}` | `ITemplate` | +| `getTemplateStats(collection, id)` | `/v1/templates/{collection}/{id}/stats` | `ITemplateStats` | +| `getTemplateLogs(collection, id, page, limit, order)` | `/v1/templates/{collection}/{id}/logs` | `ILog[]` | +| `getTransfers(options, page, limit)` | `/v1/transfers` | `ITransfer[]` | +| `countTransfers(options)` | `/v1/transfers/_count` | `number` | +| `getOffers(options, page, limit)` | `/v1/offers` | `IOffer[]` | +| `countOffers(options)` | `/v1/offers/_count` | `number` | +| `getOffer(id)` | `/v1/offers/{id}` | `IOffer` | +| `getAccounts(options, page, limit)` | `/v1/accounts` | `Array<{ account, assets }>` | +| `countAccounts(options)` | `/v1/accounts/_count` | `number` | +| `getAccount(account, options)` | `/v1/accounts/{account}` | `IAccountStats` | +| `getAccountCollection(account, collection)` | `/v1/accounts/{account}/{collection}` | `IAccountCollectionStats` | +| `getBurns(options, page, limit)` | `/v1/burns` | `Array<{ account, assets }>` | +| `getAccountBurns(account, options)` | `/v1/burns/{account}` | `IAccountStats` | +| `fetchEndpoint(path, args)` | any path | the raw `data` payload | +| `countEndpoint(path, args)` | any `_count` path | `number` | + +Options are typed per entity (`AssetsApiParams`, `TemplateApiParams`, `CollectionApiParams`, and so on), each carrying the filter, greylist, boundary, `sort`, and `order` fields that route accepts; `sort` and `order` values are the `AssetsSort`/`OrderParam` string enums exported from the root. + +Source: atomicassets-sdk (v2.1.1, 5c70c62) src/API/Explorer/index.ts:130-256 (getters and their routes), src/API/Explorer/index.ts:206-208 (`getTemplateStats` naming its second parameter `id`), src/API/Explorer/index.ts:314-318 (`countEndpoint` appends `/_count`), src/API/Explorer/Params.ts, src/API/Explorer/Enums.ts + +### Path segments and query keys are percent-encoded + +Every caller-supplied path segment goes through `encodeURIComponent` where the path is assembled, so an asset id, collection, schema, template or account name carrying `/`, `?` or `#` cannot escape its own segment and rewrite the request target. The query side encodes both the key and the value, because the key is caller-supplied too: `buildDataOptions` splices a data-filter key and type into it, and an unencoded `&` or `=` there would smuggle extra parameters into the query. A hand-rolled URL that skips either step is the flaw this closes; see [Query the API and chain tables](../../guides/querying-the-api.md#percent-encode-every-caller-supplied-url-part) ("Percent-encode every caller-supplied URL part"). + +Source: atomicassets-sdk (v2.1.1, 5c70c62) src/API/Explorer/index.ts:32-63 (`encodeSegment`), src/API/Explorer/index.ts:143-256 (every path segment encoded), src/API/Explorer/index.ts:274-277 (query key and value encoded) + +### An empty or dot path segment is refused before the request + +Encoding alone does not cover every value that can move a request. `.` and `..` are unreserved characters, so `encodeURIComponent` leaves them intact, and the URL parser inside `fetch` then resolves the dot segment away and aims the read at a different route on the same origin. An empty id leaves a bare trailing slash, which turns a single-item route into the list route above it. In both cases the caller reads rows it never asked for and sees no failure at all, which is why this is refused rather than encoded. + +`encodeSegment` therefore rejects three values before the path is assembled. An empty string, `.`, and `..` each throw an error whose message names the argument and the offending value, for example `asset id ".." is not a valid path segment: it is empty or a dot segment, so it would rewrite the request path`, and a `null` or `undefined` argument throws `asset id is required` rather than reaching the path as the literal segment `undefined`. The throw is a plain `Error` and not an `ApiError`, because the guard fires while the path is built and no response exists yet to carry a status. Nothing is sent. The sixteen getters that place a caller value in a path all carry the check, across the asset, collection, schema, template, offer, and account routes, and a two-segment lookup checks both of its segments. + +A dot inside a segment is untouched, so an Antelope name such as `alice.gg` still reaches the request unchanged. Only a segment that is exactly `.` or `..` is a dot segment. + +Source: atomicassets-sdk (v2.1.1, 5c70c62) src/API/Explorer/index.ts:32-45 (why a dot segment survives encoding), src/API/Explorer/index.ts:46-63 (`encodeSegment` and both throws), src/API/Explorer/index.ts:143-255 (the sixteen guarded getters, `getSchema`/`getTemplate`/`getAccountCollection` guarding two segments each), test/explorer-url.test.ts:106-188 (nothing is sent, the message text, the dotted name passing) + +### Typed data filters and the long-query POST switch + +The final `data` argument on `getAssets`/`getTemplates` targets the on-chain data filters: each entry `{ key, value, type? }` becomes a query field keyed `data.`, `data:number.`, or `data:bool.` by the value's JS type (`type` defaults to `data`, and can be set to `template_data`/`immutable_data`/`mutable_data`). Percent-encoding then puts the colon on the wire as `%3A`, so `data:number.id=4` is sent as `data%3Anumber.id=4`. Requests whose query string reaches 1000 characters are sent as a POST with a JSON body instead of a GET, transparently to the caller. + +Source: atomicassets-sdk (v2.1.1, 5c70c62) src/API/Explorer/index.ts:65-81 (`buildDataOptions`), src/API/Explorer/index.ts:277 (`encodeURIComponent` on the key), src/API/Explorer/index.ts:280-296 (the 1000-character GET/POST switch) + +### Reads against WAX mainnet + +Live reads against `https://wax.api.atomicassets.io`: ```js import { ExplorerApi, explorerApiForNetwork } from '@atomichub/atomicassets'; @@ -45,9 +114,9 @@ await api.getTemplates({ collection_name: 'pixeltycoons' }, 1, 2); // -> ITemplate[] of length 2 ``` -Every getter throws `ApiError` on a non-200 response or a `success: false` body, so a rejected promise is the failure signal; there is no undefined-on-error path. List endpoints reject `limit` above the deployment cap (100 on the reference deployment) with HTTP 400, surfaced as an `ApiError`; bound `limit` to 100 and page through. See [atomicassets-api HTTP API](../api.md#list-endpoints-cap-limit-at-100) ("List endpoints cap limit at 100"). +Every getter throws `ApiError` on a non-200 response or a `success: false` body, so a rejected promise is the failure signal; there is no undefined-on-error path. A refused path segment is the one failure that is not an `ApiError`, because it is raised before a request exists. List endpoints reject `limit` above the deployment cap (100 on the reference deployment) with HTTP 400, surfaced as an `ApiError`; bound `limit` to 100 and page through. See [atomicassets-api HTTP API](../api.md#list-endpoints-cap-limit-at-100) ("List endpoints cap limit at 100"). -Source: atomicassets-sdk (main, 80580c5) src/API/Explorer/index.ts (constructor, getters, `buildDataOptions`, the 1000-char GET/POST switch, `fetchEndpoint` error handling), src/API/Explorer/Params.ts, src/API/Explorer/Enums.ts; live reads against `https://wax.api.atomicassets.io` +Source: atomicassets-sdk (v2.1.1, 5c70c62) src/API/Explorer/index.ts:303-309 (`fetchEndpoint` error handling); live reads against `https://wax.api.atomicassets.io` ## RpcApi reads chain tables directly, with a rate-limited queue and a cache @@ -57,7 +126,7 @@ The getters return lazy wrapper objects, not plain rows. `getAsset(owner, id)` r Prefer `ExplorerApi` for anything the indexer answers: filtered lists, search, sort orders, counts, stats, and cross-owner enumeration (the `assets` table is scoped by owner on chain, so there is no chain-side path from a collection to its assets without knowing the owners). Reach for `RpcApi` when you need the unindexed chain truth: reading a specific row without indexer lag, or running against a node when no atomicassets-api deployment is available. The two clients do not share a cache. -Source: atomicassets-sdk (main, 80580c5) src/API/Rpc/index.ts (constructor, getters, `getTableRows`), src/API/Rpc/Queue.ts (`setInterval(..., ceil(1000/requestLimit))`, default 4), src/API/Rpc/RpcCache.ts (15-minute TTL), src/API/Rpc/Asset.ts (lazy wrapper, precedence in `data()`) +Source: atomicassets-sdk (v2.1.1, 5c70c62) src/API/Rpc/index.ts:32-44 (constructor, queue construction), src/API/Rpc/index.ts:80-166 (getters), src/API/Rpc/index.ts:172-181 (`getTableRows` forcing `limit: 101`, `json: true`), src/API/Rpc/Queue.ts:15 and :130-137 (`setInterval(..., ceil(1000/requestLimit))`, default 4), src/API/Rpc/RpcCache.ts:124 (15-minute TTL), src/API/Rpc/Asset.ts (lazy wrapper, precedence in `data()`) ## Serialization decodes table blobs; the attribute-map helpers build action data @@ -65,6 +134,8 @@ Two distinct jobs use two distinct helpers, and mixing them is a common error. ` Building action data is the other direction and does not produce bytes. `createAttributeMap(values, types)` and `toAttributeMap(values, schemaFormat)` turn a plain object into the `ATTRIBUTE_MAP` shape the contract's action arguments expect: an array of `{ key, value: [variantName, value] }` pairs, where the variant name is the ABI's `ATOMIC_ATTRIBUTE` name for the field's type. `createAttributeMap` takes an explicit per-key type map; `toAttributeMap` derives the types from a schema format. The chain, not the SDK, serializes this map to bytes during transaction execution, so action `immutable_data`/`mutable_data`/`data` fields are attribute-map arrays, never `serialize()` output. +Decoding accepts both spellings of the attribute pair. The on-chain `pair_string_ATOMIC_ATTRIBUTE` struct is `key`/`value` in the v1 mainnet ABI and in the v2 release ABI alike, but CDT 4.1 and newer emit the C++ member names `first`/`second` from abigen, and the contract's release build patches them back before the ABI ships. An ABI taken from an unpatched build hands back the other spelling, so `DecodedAttributeMap` admits both and a caller need not normalize first. + Round-trip run against a live schema format read from `https://wax.api.atomicassets.io`, and a standalone format: ```js @@ -77,13 +148,15 @@ deserialize(serialize(obj, codec), codec); // -> { name: 'Hero', level: 42, img: 'QmABC' } (round-trips equal) ``` -Source: atomicassets-sdk (main, 80580c5) src/Serialization/index.ts (`serialize`/`deserialize`/`toByteArray`), src/Schema/index.ts (`ObjectSchema`, `CachedObjectSchema`), src/Actions/Generator.ts (`createAttributeMap`, `toAttributeMap`, `ATOMIC_ATTRIBUTE`); round-trip executed against a live schema read from `https://wax.api.atomicassets.io` +Source: atomicassets-sdk (v2.1.1, 5c70c62) src/Serialization/index.ts (`serialize`/`deserialize`/`toByteArray`), src/Schema/index.ts:56-100 (`ObjectSchema`, `CachedObjectSchema`, the 500-entry bound), src/Actions/Generator.ts:54-91 (`ATOMIC_ATTRIBUTE`), src/Actions/Generator.ts:96-118 (`createAttributeMap`), src/Actions/Generator.ts:26-36 (`DecodedAttributeMap` accepting `first`/`second`); round-trip executed against a live schema read from `https://wax.api.atomicassets.io` ## Action building: a sync authorization-free builder and an async authorization-first generator The SDK builds every AtomicAssets action as a plain object; it never signs or pushes. There are three layers. `ActionBuilder(contract)` is synchronous and authorization-free: one method per action, each returning a single `{ account, name, data }` object (`EosioSimpleAction`), for pipelines that attach authorization themselves. `ActionGenerator(contract)` wraps the same builders as `async` methods taking an `authorization` array first and returning `[{ account, name, authorization, data }]`. `ExplorerActionGenerator` (reached via `explorerApi.action`) additionally accepts plain-object data for the data-bearing actions (`createcol`, `createtempl`, `mintasset`, `setassetdata`, `setcoldata`) and serializes it to the attribute-map shape by fetching the relevant schema or collection format, so callers pass `{ name: 'Hero' }` instead of hand-building pairs. -`mintasset` on the builder takes eight positional arguments in ABI order: `authorized_minter, collection_name, schema_name, template_id, new_asset_owner, immutable_data, mutable_data, tokens_to_back` (the `ActionGenerator` form adds `authorization` as the first argument, making nine). Two data-map arguments and a backed-token array are separate, and their order matters. Note `transfer` remaps its `account_from`/`account_to` parameters to the contract's `from`/`to` data keys. +The two packages differ here, and a caller composing both trips on it: an `ActionBuilder` method returns one action object, while every `MarketActionBuilder` method in [@atomichub/atomicmarket SDK](atomicmarket.md) returns an array of them. Spread the market builder's result and push the assets builder's. + +`mintasset` on the builder takes eight positional arguments in ABI order: `authorized_minter, collection_name, schema_name, template_id, new_asset_owner, immutable_data, mutable_data, tokens_to_back` (the `ActionGenerator` form adds `authorization` as the first argument, making nine). Two data-map arguments and a backed-token array are separate, and their order matters. `transfer(from, to, asset_ids, memo)` names its first two parameters after the ABI fields they fill, so nothing is remapped on the way through. ```js import { ActionBuilder, createAttributeMap } from '@atomichub/atomicassets'; @@ -97,25 +170,48 @@ builder.mintasset('minteracct', 'pixeltycoons', 'heroes', 4, 'targetacct', immut // immutable_data: [{ key: 'name', value: ['string', 'Hero'] }], mutable_data: [], tokens_to_back: [] } } ``` -Source: atomicassets-sdk (main, 80580c5) src/Actions/Generator.ts (`ActionBuilder`, `ActionGenerator`, 8-arg `mintasset`, `transfer` from/to remap), src/Actions/Explorer.ts (`ExplorerActionGenerator` auto-serialization); `mintasset` output executed locally +Source: atomicassets-sdk (v2.1.1, 5c70c62) src/Actions/Generator.ts:14-18 (`EosioSimpleAction`), src/Actions/Generator.ts:524-526 (`_action` returning one object), src/Actions/Generator.ts:373-384 (8-argument `mintasset`), src/Actions/Generator.ts:516-518 (`transfer(from, to, asset_ids, memo)`), src/Actions/Generator.ts:529-536 and :855-857 (`ActionGenerator`, `_authorize` returning an array), src/Actions/Explorer.ts (`ExplorerActionGenerator` auto-serialization); `mintasset` output executed locally + +### Numeric parameters are checked against their ABI type and throw + +The builders check almost nothing, deliberately: names, symbols, and 64-bit ids are forwarded unchecked because the chain rejects a malformed one with an error that names it. The numeric parameters are the exception, because a bad value there neither throws nor survives the trip. Action data reaches a signing library as JSON, where `NaN` and `Infinity` have no form, so `max_supply: NaN` is written as `"max_supply": null` and the mistake is erased before anything on chain can name it. A fractional or negative value for an integer field is the quieter version: it serializes intact and surfaces, if at all, in a chain error naming neither the call nor the field. + +Each numeric parameter is therefore checked against the ABI type of the field it fills and throws a `SerializationError` naming that field: + +| Parameter | Checked as | Where | +| --- | --- | --- | +| `template_id` | int32, so `-1` stays available as the no-template sentinel | `mintasset`, `deltemplate`, `locktemplate`, `settempldata`, `redtemplmax` | +| `max_supply` | uint32, refusing a fractional or negative supply | `createtempl`, `createtempl2` | +| `new_max_supply` | uint32 | `redtemplmax` | +| `market_fee` | float64, so only finiteness is checkable | `createcol`, `setmarketfee` | + +The error text carries the field and the offending value, for example `max_supply 1.5 is not a uint32 (an integer 0 to 4294967295)`. What the contract requires beyond the ABI width, such as which market fee a collection may charge, stays the chain's to enforce and returns a legible error of its own. + +Source: atomicassets-sdk (v2.1.1, 5c70c62) src/Actions/Generator.ts:126-157 (`INT32_MIN`/`INT32_MAX`/`UINT32_MAX`, `assertFinite`, `assertInt32`, `assertUint32`), src/Actions/Generator.ts:276 and :451 (`market_fee`), src/Actions/Generator.ts:299 and :311 (`max_supply`), src/Actions/Generator.ts:330, :355, :394-395, :477 (`template_id`, `new_max_supply`), src/Actions/Generator.ts:379 (`mintasset` `template_id`) + +### Native backing is deprecated on the action and on the mint parameter + +`backasset` carries a `@deprecated` tag on both the builder and the generator, and `mintasset` carries the same tag on its `tokens_to_back` parameter. AtomicAssets v2 ends `mintasset` with a check that `tokens_to_back` is empty and guards `backasset` the same way, so both abort there. Both still execute on a chain that has not migrated, which means a call that works says the chain has not arrived yet rather than that the path is supported. Pass `[]` and back nothing. The contract-side rule and its abort message are in [Create a collection and mint assets](../../guides/asset-lifecycle.md#mint-an-asset-mintasset) ("Mint an asset: mintasset"). + +Source: atomicassets-sdk (v2.1.1, 5c70c62) src/Actions/Generator.ts:238-240 (`backasset` on the builder), src/Actions/Generator.ts:581-585 (`backasset` on the generator), src/Actions/Generator.ts:358-372 (`mintasset` `tokens_to_back`) ## Network factories carry AtomicHub's public hosts `explorerApiForNetwork(network, options?)` and `rpcApiForNetwork(network, contract?, options?)` construct a preconfigured client against AtomicHub's public endpoints, and `NETWORK_ENDPOINTS` exposes the host map. The valid `AtomicHubNetwork` keys are `wax`, `wax-testnet`, `vaulta`, `xpr`, `xpr-testnet`, and `jungle4`. Each key currently maps its `api` and `rpc` to the same host (for example `wax` to `https://wax.api.atomicassets.io`), and the split is kept so the shapes survive if the hosts ever diverge. Any compatible deployment can still be passed straight to the `ExplorerApi`/`RpcApi` constructors instead of using a factory. -Source: atomicassets-sdk (main, 80580c5) src/Networks.ts (`AtomicHubNetwork`, `NETWORK_ENDPOINTS`, `explorerApiForNetwork`, `rpcApiForNetwork`); `wax` factory verified live +Source: atomicassets-sdk (v2.1.1, 5c70c62) src/Networks.ts:9-48 (`AtomicHubNetwork`, `NETWORK_ENDPOINTS`, `explorerApiForNetwork`, `rpcApiForNetwork`); `wax` factory verified live ## Error types are exported for instanceof matching -Failures throw typed `Error` subclasses, all exported from the root so consumers can `instanceof`-match them. `ApiError` carries an `isApiError = true` flag and a numeric `status` (the HTTP status, or 500 for a transport failure); it is what every `ExplorerApi` getter throws. `RpcError` wraps a node error response and pulls the deepest available message out of the nodeos `error.details`/`processed.except` envelope. `SerializationError`, `DeserializationError`, and `SchemaError` cover the codec paths, and `ExplorerError` the explorer-action path. Match on `ApiError` and read `.status` to distinguish an over-limit 400 from a 404 from a transport 500. +Failures throw typed `Error` subclasses, all exported from the root so consumers can `instanceof`-match them. `ApiError` carries an `isApiError = true` flag and a numeric `status` (the HTTP status, or 500 for a transport failure); it is what every `ExplorerApi` getter throws. `RpcError` wraps a node error response and pulls the deepest available message out of the nodeos `error.details`/`processed.except` envelope. `SerializationError`, `DeserializationError`, and `SchemaError` cover the codec paths and the numeric guards above, and `ExplorerError` the explorer-action path. Match on `ApiError` and read `.status` to distinguish an over-limit 400 from a 404 from a transport 500. -Source: atomicassets-sdk (main, 80580c5) src/Errors/ApiError.ts, src/Errors/RpcError.ts, src/Errors/{Serialization,Deserialization,Schema,Explorer}Error.ts, src/index.ts (root re-exports) +Source: atomicassets-sdk (v2.1.1, 5c70c62) src/Errors/ApiError.ts, src/Errors/RpcError.ts, src/Errors/{Serialization,Deserialization,Schema,Explorer}Error.ts, src/index.ts:32-37 (root re-exports) ## SDK action output composes directly with @wharfkit session.transact The action objects the generator returns are already in the shape WharfKit's `session.transact({ actions })` accepts: `{ account, name, authorization, data }`, with each authorization entry an `{ actor, permission }` object matching the SDK's `EosioAuthorizationObject`. So a signer flow is `session.transact({ actions: await explorerApi.action.then(a => a.mintasset(auth, ...)) })`, where `auth = [{ actor, permission }]`. Because the data-bearing actions carry the attribute-map shape (not serialized bytes), the ABI encoding happens inside WharfKit and nodeos at transact time, the same as any hand-built action. The SDK's job ends at producing the action array. For WharfKit's table-read and authority behavior, and its eosjs-migration caveats, see [@wharfkit/antelope client behavior](../wharfkit.md). -Source: atomicassets-sdk (main, 80580c5) src/Actions/Generator.ts (`EosioActionObject`, `EosioAuthorizationObject`, `_authorize`) +Source: atomicassets-sdk (v2.1.1, 5c70c62) src/Actions/Generator.ts:4-18 (`EosioActionObject`, `EosioAuthorizationObject`), src/Actions/Generator.ts:855-857 (`_authorize`) ## When to use the SDK versus raw HTTP or WharfKit table reads diff --git a/reference/sdk/atomicmarket.md b/reference/sdk/atomicmarket.md index 2b33fa2..a76543c 100644 --- a/reference/sdk/atomicmarket.md +++ b/reference/sdk/atomicmarket.md @@ -1,13 +1,13 @@ --- -scope: "@atomichub/atomicmarket JavaScript/TypeScript SDK: AtomicMarketApi reads of sales, auctions, and buyoffers, the v2 royalty read layer, and action building" +scope: "@atomichub/atomicmarket JavaScript/TypeScript SDK: AtomicMarketApi reads, the royalty layer and payout ledger, every v2 action, the composers, and delphi math" depends-on: [reference/api.md, reference/atomicmarket/fees-and-royalties.md, reference/sdk/atomicassets.md] key-modules: - - "@atomichub/atomicmarket 2.0.0 (atomicmarket-sdk main, 278bdfa): src/index.ts, src/API/Explorer/index.ts, src/API/Explorer/Objects.ts, src/Actions/Generator.ts, src/Networks.ts" + - "@atomichub/atomicmarket 2.4.1 (atomicmarket-sdk v2.4.1, 437300b): src/index.ts, src/API/Explorer/index.ts, src/API/Explorer/Objects.ts, src/API/Explorer/Enums.ts, src/API/Explorer/Params.ts, src/Actions/Generator.ts, src/Actions/Delphi.ts, src/Actions/Symbols.ts, src/Tables.ts, src/Networks.ts" --- # @atomichub/atomicmarket SDK -The official JavaScript/TypeScript client for the AtomicMarket marketplace contract. It reads sales, auctions, buyoffers, and the v2 royalty configuration over the hosted API, and builds the v2 royalty-config actions for a signer. Version-sensitive facts below were read from the 2.0.0 source tree; re-verify against current source after an upgrade. +The official JavaScript/TypeScript client for the AtomicMarket marketplace contract. It reads sales, auctions, buyoffers, marketplaces, the v2 royalty configuration, and the settled royalty payout ledger over the hosted API, builds every v2 contract action for a signer, composes the multi-action listing and purchase flows, and derives the settlement amount an oracle-priced purchase has to deposit. Version-sensitive facts below were read from the 2.4.1 source tree; re-verify against current source after an upgrade. ``` npm install @atomichub/atomicmarket @@ -15,39 +15,89 @@ npm install @atomichub/atomicmarket ## The package depends on @atomichub/atomicassets at runtime -`@atomichub/atomicmarket` has exactly one runtime dependency, `@atomichub/atomicassets`, and re-exports the shared eosio action shapes (`EosioActionObject`, `EosioAuthorizationObject`), the network presets (`AtomicHubNetwork`, `NETWORK_ENDPOINTS`), and the AtomicAssets response types it composes with (a market asset is an AtomicAssets asset plus sale/auction/price fields). Installing the market SDK therefore pulls the assets SDK, and the two share one source of truth for those types. The package ships dual ESM/CJS builds and requires Node `>=20`, like the assets SDK. +`@atomichub/atomicmarket` has exactly one runtime dependency, `@atomichub/atomicassets`, and re-exports the shared eosio action shapes (`EosioActionObject`, `EosioAuthorizationObject`), the network presets (`AtomicHubNetwork`, `NETWORK_ENDPOINTS`), and the AtomicAssets response types it composes with (a market asset is an AtomicAssets asset plus sale/auction/price fields). The dependency is load-bearing rather than incidental: the flow composers below build AtomicAssets `createoffer` and `transfer` actions through that package's own `ActionBuilder`. Installing the market SDK therefore pulls the assets SDK, and the two share one source of truth for those types. The package ships dual ESM/CJS builds, declares `sideEffects: false`, and requires Node `>=20`, like the assets SDK. -Source: atomicmarket-sdk (main, 278bdfa) package.json (single `dependencies` entry `@atomichub/atomicassets`), src/index.ts and src/Actions/Generator.ts (re-exports of the assets eosio types), src/Networks.ts (re-exported presets) +Source: atomicmarket-sdk (v2.4.1, 437300b) package.json:32 (`sideEffects: false`), package.json:34-35 (`engines.node >=20`), package.json:65-66 (single `dependencies` entry), src/Actions/Generator.ts:1-8 (the assets `ActionBuilder` import and the eosio type re-exports), src/Networks.ts:1-10 (re-exported presets) ## AtomicMarketApi reads sales, auctions, buyoffers, and marketplaces -`AtomicMarketApi` wraps the hosted `/atomicmarket` API. The constructor takes `(endpoint, namespace, { fetch? })`, with `namespace` the `atomicmarket` API namespace; the `marketApiForNetwork` factory supplies both. The listing getters mirror the AtomicAssets SDK shape: `getSales(options, page, limit, data)`, `getSale(id)`, `getAuctions`, `getAuction`, `getBuyoffers`, `getBuyoffer`, each with `getXLogs` and `countX` variants, plus `getMarketplaces`/`getMarketplace` and `getConfig`. List getters default to `page = 1`, `limit = 100`, and options are typed per listing (`SaleApiParams`, `AuctionApiParams`, `BuyofferApiParams`). The `state` field on each listing is a typed enum, and it differs by listing type; the SDK's `SaleState`/`AuctionState`/`BuyofferState` enums ship as runtime values, and the authoritative per-endpoint meanings are in [atomicassets-api HTTP API](../api.md#the-state-field-means-something-different-on-each-listing-endpoint) ("The `state` field means something different on each listing endpoint"). Every getter throws `ApiError` (carrying `isApiError` and a numeric `status`) on a non-200 or `success: false` response. +`AtomicMarketApi` wraps the hosted `/atomicmarket` API. The constructor takes `(endpoint, namespace, { fetch? })`, with `namespace` the `atomicmarket` API namespace; the `marketApiForNetwork` factory supplies both. Every getter returns the response `data` payload already unwrapped, and list getters default to `page = 1` and `limit = 100`. -Live reads against `https://wax.api.atomicassets.io` (WAX mainnet): +| Method | Route | Returns | +| --- | --- | --- | +| `getSales(options, page, limit, data)` | `/v1/sales` | `ISale[]` | +| `countSales(options, data)` | `/v1/sales/_count` | `number` | +| `getSale(id)` | `/v1/sales/{id}` | `ISale` | +| `getSaleLogs(id, page, limit, order)` | `/v1/sales/{id}/logs` | `ILog[]` | +| `getSalesV2(options, page, limit, data)` | `/v2/sales` | `ISale[]` | +| `countSalesV2(options, data)` | `/v2/sales/_count` | `number` | +| `getAuctions(options, page, limit, data)` | `/v1/auctions` | `IAuction[]` | +| `countAuctions(options, data)` | `/v1/auctions/_count` | `number` | +| `getAuction(id)` | `/v1/auctions/{id}` | `IAuction` | +| `getAuctionLogs(id, page, limit, order)` | `/v1/auctions/{id}/logs` | `ILog[]` | +| `getBuyoffers(options, page, limit, data)` | `/v1/buyoffers` | `IBuyoffer[]` | +| `countBuyoffers(options, data)` | `/v1/buyoffers/_count` | `number` | +| `getBuyoffer(id)` | `/v1/buyoffers/{id}` | `IBuyoffer` | +| `getBuyofferLogs(id, page, limit, order)` | `/v1/buyoffers/{id}/logs` | `ILog[]` | +| `getMarketplaces()` | `/v1/marketplaces` | `IMarketplace[]` | +| `getMarketplace(name)` | `/v1/marketplaces/{name}` | `IMarketplace` | +| `getConfig()` | `/v1/config` | `IMarketConfig` | +| `getRoyaltyConfig(collection)` | `/v1/royalties/{collection}` | `IRoyaltyConfig` or `null` | +| `getRoyaltyTemplateRules(collection, page, limit)` | `/v1/royalties/{collection}/templates` | `IRoyaltyTemplateRule[]` | +| `getRoyaltyAttributeRules(collection, page, limit)` | `/v1/royalties/{collection}/attributes` | `IRoyaltyAttributeRule[]` | +| `getRoyaltyPayouts(options, page, limit)` | `/v1/royalties/payouts` | `IRoyaltyPayout[]` | +| `countRoyaltyPayouts(options)` | `/v1/royalties/payouts/_count` | `number` | +| `getRoyaltyAccount(account, options)` | `/v1/royalties/accounts/{account}` | `IRoyaltyAccountTotal[]` | +| `getPriceHistory(options)` | `/v1/prices/sales` | per-sale price rows | +| `getPriceHistoryByDays(options)` | `/v1/prices/sales/days` | daily average and median rows | +| `getTemplatePriceStats(options)` | `/v1/prices/templates` | per-template price stats | +| `getAssetPrices(options, data)` | `/v1/prices/assets` | per-asset price stats | +| `getAssets(options, page, limit, data)` | `/v1/assets` | `IMarketAsset[]` | +| `getAsset(id)` | `/v1/assets/{id}` | `IMarketAsset` | +| `getTransfers(options, page, limit)` | `/v1/transfers` | `IMarketTransfer[]` | +| `getOffers(options, page, limit)` | `/v1/offers` | `IMarketOffer[]` | +| `countOffers(options)` | `/v1/offers/_count` | `number` | +| `getOffer(id)` | `/v1/offers/{id}` | `IMarketOffer` | +| `fetchEndpoint(path, args)` | any path | the raw `data` payload | +| `countEndpoint(path, args)` | any `_count` path | `number` | -```js -import { AtomicMarketApi, marketApiForNetwork } from '@atomichub/atomicmarket'; +Two facts about that surface are not inventory. The `state` field on each listing is a typed enum and it differs by listing type: state 1 is Listed for a sale, Declined for a buyoffer, and Canceled for a template buyoffer, so reusing one listing type's enum against another reads plausibly and returns the wrong rows. `SaleState`, `AuctionState`, `BuyofferState`, and `TemplateBuyofferState` ship as runtime values, and the authoritative per-endpoint meanings are in [atomicassets-api HTTP API](../api.md#the-state-field-means-something-different-on-each-listing-endpoint) ("The `state` field means something different on each listing endpoint"). And every getter throws `ApiError` (carrying `isApiError` and a numeric `status`) on a non-200 or `success: false` response, with the single exception of `getRoyaltyConfig` below; there is no undefined-on-error path. A refused path id is the one failure that is not an `ApiError`, because it is raised before a request exists. -const market = marketApiForNetwork('wax'); +Options are typed per listing (`SaleApiParams`, `AuctionApiParams`, `BuyofferApiParams`), each widening `state` to a string so a comma-joined multi-state filter is expressible. `MarketOfferApiParams` does the same for offers, which the AtomicAssets package pins to its `OfferState` enum; `getOffers` and `countOffers` share that widened surface. The two payout readers take `RoyaltyPayoutApiParams` and `RoyaltyAccountApiParams`, described under the payout ledger below. -await market.getSales({ state: '3', sort: 'sale_id', order: 'desc' }, 1, 2); -// -> ISale[] of length 2 +Source: atomicmarket-sdk (v2.4.1, 437300b) src/API/Explorer/index.ts:69-237 (constructor, getters, routes), src/API/Explorer/index.ts:266-272 (`ApiError` on non-200 or `success: false`), src/API/Explorer/index.ts:277-281 (`countEndpoint` appends `/_count`), src/API/Explorer/Enums.ts:1-46 (the four state enums and the per-listing divergence), src/API/Explorer/Params.ts:54-59 (`MarketOfferApiParams` widening `state`), src/API/Explorer/Objects.ts (`ISale`, `IAuction`, `IBuyoffer`, `IMarketConfig`); live reads against `https://wax.api.atomicassets.io` -await market.getSale('173548902'); -// -> { sale_id: '173548902', seller: 'alienz251212', state: 3, -// price: { amount: '992994', token_symbol: 'WAX', ... }, collection: { collection_name: 'rustveil', ... }, ... } +### The materialized /v2/sales route -await market.getConfig(); -// -> { maker_market_fee: 0.01, taker_market_fee: 0.01, version: '1.3.3', ... } -``` +`getSalesV2` and `countSalesV2` read `/atomicmarket/v2/sales`, the API's materialized sales index. Its row shape is identical to `/v1/sales`, which is why both getters return `ISale`, and it takes the same filters. The hosted WAX deployment serves both routes, and the OpenAPI document behind its Swagger UI describes only the `/v2` one. See [atomicassets-api HTTP API](../api.md#two-sales-list-routes-answer-on-the-hosted-deployment) ("Two sales list routes answer on the hosted deployment"). + +Source: atomicmarket-sdk (v2.4.1, 437300b) src/API/Explorer/index.ts:92-100 (`getSalesV2`, `countSalesV2`, and the note that the row shape matches `/v1/sales`); live reads of `https://wax.api.atomicassets.io/atomicmarket/v2/sales` (200) and `/atomicmarket/v2/sales/_count` (200) + +### Path ids and query keys are percent-encoded + +Every caller-supplied path id goes through `encodeURIComponent`, and so does each query key and value, the key mattering because `buildDataOptions` splices a data-filter key and type into it. A value carrying `/`, `?`, `#`, `&`, or `=` therefore cannot reshape the request. A hand-rolled URL that skips either step is the flaw this closes; see [Query the API and chain tables](../../guides/querying-the-api.md#percent-encode-every-caller-supplied-url-part) ("Percent-encode every caller-supplied URL part"). + +The `data` argument on the listing getters produces the same typed-filter keys the assets client does: each `{ key, value, type? }` entry becomes `data.`, `data:number.`, or `data:bool.` by the value's JS type, and encoding puts the colon on the wire as `%3A`, so `data:number.level=1` is sent as `data%3Anumber.level=1`. + +Source: atomicmarket-sdk (v2.4.1, 437300b) src/API/Explorer/index.ts:29-43 (`encodeSegment`), src/API/Explorer/index.ts:85-236 (every path id encoded), src/API/Explorer/index.ts:244-256 (query key and value encoded), src/API/Explorer/index.ts:45-61 (`buildDataOptions` and the typed-filter key shapes) + +### An empty or dot path id is refused before the request + +Encoding alone does not cover every value that can move a request. `.` and `..` are unreserved characters, so `encodeURIComponent` leaves them intact, and the URL parser inside `fetch` then resolves the dot segment away: `getRoyaltyAccount('..')` would request `/v1/royalties/`, and an empty id turns a single-row route into its list. Both land on a real route on the same origin, so the caller reads rows it never asked for and sees no failure. + +`encodeSegment` therefore rejects three values before the path is assembled. An empty string, `.`, and `..` each throw an error naming the argument and the offending value, for example `sale id ".." is not an id: it is empty or a dot segment, so it would rewrite the request path`, and a `null` or `undefined` argument throws `sale id is required`. The throw is a plain `Error` and not an `ApiError`, because the guard fires while the path is built and no response exists yet to carry a status. Nothing is sent. The thirteen readers that place a caller value in a path all carry the check, across the sale, auction, buyoffer, marketplace, royalty, asset, and offer routes. -Source: atomicmarket-sdk (main, 278bdfa) src/API/Explorer/index.ts (constructor, listing getters, `getConfig`), src/API/Explorer/Enums.ts (state enums), src/API/Explorer/Objects.ts (`ISale`, `IAuction`, `IBuyoffer`, `IMarketConfig`); live reads against `https://wax.api.atomicassets.io` +Two consequences are worth holding. `getRoyaltyConfig` maps only an `ApiError` of status 416 to `null`, so the guard's plain `Error` travels out of it rather than reading as a collection with no royalty config. And a dot inside a segment is untouched, so an Antelope name such as `alice.gg` still reaches the request unchanged; only a segment that is exactly `.` or `..` is a dot segment. -## The v2 royalty read layer returns config, template rules, and attribute rules +Source: atomicmarket-sdk (v2.4.1, 437300b) src/API/Explorer/index.ts:12-28 (why a dot segment survives encoding), src/API/Explorer/index.ts:29-43 (`encodeSegment` and both throws), src/API/Explorer/index.ts:85-236 (the thirteen guarded readers), src/API/Explorer/index.ts:150-160 (`getRoyaltyConfig` catching only a 416 `ApiError`), test/path-segments.test.ts:21-23 and :29-43 (the message text and the thirteen entries), test/path-segments.test.ts:62-63, :94, :104-110 and :113-125 (not an `ApiError`, the missing-value message, nothing sent, the dotted name passing) -Three getters read the AtomicMarket v2 royalty configuration that backs the fee split documented in `reference/atomicmarket/fees-and-royalties.md`. `getRoyaltyConfig(collection)` returns the founders list plus the founders/templates/attributes split, `getRoyaltyTemplateRules(collection, page, limit)` the per-template recipient rules, and `getRoyaltyAttributeRules(collection, page, limit)` the attribute-match rules (each carrying its raw contract variant `value` tuple, for example `["string", "legendary"]`, preserved verbatim). `getRoyaltyConfig` catches the API's HTTP 416 (a collection with no royalty config) and returns `null` rather than throwing, so `null` is the normal "no config" signal and any other status still raises `ApiError`. +## The v2 royalty read layer covers config, rules, and settled payouts -This read layer is a v2 API surface. WAX mainnet still runs the V1 contracts and its reference deployment does not serve `/atomicmarket/v1/royalties/*` at all (the route returns HTTP 404, which surfaces as an `ApiError` rather than `null`); the endpoints answer on the V2 deployments such as WAX testnet. Point `getRoyaltyConfig` at a deployment that carries V2, matching the mainnet-versus-testnet split in `guides/querying-the-api.md`. +Three getters read the AtomicMarket v2 royalty configuration that backs the fee split documented in [AtomicMarket fees and royalties](../atomicmarket/fees-and-royalties.md). `getRoyaltyConfig(collection)` returns the founders list plus the founders/templates/attributes split, `getRoyaltyTemplateRules(collection, page, limit)` the per-template recipient rules, and `getRoyaltyAttributeRules(collection, page, limit)` the attribute-match rules (each carrying its raw contract variant `value` tuple, for example `["string", "legendary"]`, preserved verbatim). What a collection has actually paid is a separate read, covered by the payout ledger below. + +`getRoyaltyConfig` is the one getter that does not throw on every failure. It catches HTTP 416 and returns `null`, because 416 is the API's answer for a collection with no royalty config, which is a normal result rather than an error. Any other status still raises `ApiError`. + +That mapping is what makes the mainnet case quiet rather than loud. WAX mainnet still runs the V1 contracts, and its reference deployment answers `/atomicmarket/v1/royalties/{collection}` with HTTP 416 and `{"success": false, "message": "Royalty config not found"}` for every collection, so `getRoyaltyConfig` returns `null` there and never raises. Guard on `null`, not on `ApiError`: a caller who treats `null` as "this collection has no royalties" reads a mainnet-wide "the route has no data" as a per-collection fact. Point the getter at a deployment carrying V2, such as WAX testnet, matching the mainnet-versus-testnet split in [Query the API and chain tables](../../guides/querying-the-api.md). Live reads against the WAX testnet deployment `https://test.wax.api.atomicassets.io`: @@ -55,27 +105,115 @@ Live reads against the WAX testnet deployment `https://test.wax.api.atomicassets const test = marketApiForNetwork('wax-testnet'); await test.getRoyaltyConfig('royaltycol11'); -// -> { collection_name: 'royaltycol11', +// -> { market_contract: 'atomicmarket', collection_name: 'royaltycol11', // founders: [{ weight: 1, recipient: 'jacktestr125' }, { weight: 3, recipient: 'pe2etestacct' }], -// attribute_mode: 0, split_founders: '2', split_templates: '1', split_attributes: '1', ... } +// attribute_mode: 0, split_founders: '2', split_templates: '1', split_attributes: '1', +// updated_at_block: '414895352', updated_at_time: '1783371583500', created_at_block: ..., created_at_time: ... } await test.getRoyaltyConfig('farmmetricsx'); // -> null (HTTP 416, no royalty config for this collection) await test.getRoyaltyAttributeRules('royaltycol11'); -// -> [{ rule_id: '2', source: 0, field: 'rarity', value: ['string', 'legendary'], -// weight: '1', recipients: [{ weight: 1, recipient: 'jacktestr125' }], ... }] +// -> [{ market_contract: 'atomicmarket', collection_name: 'royaltycol11', +// rule_id: '2', source: 0, field: 'rarity', value: ['string', 'legendary'], +// weight: '1', recipients: [{ weight: 1, recipient: 'jacktestr125' }], +// lookup_hash: '68ec3427c453d24cfd9faaa1db9d39533ab06b8127bb09a6f2958e5c26ebce74', ... }] ``` Note the split fields and rule weights come back as decimal strings while recipient weights inside pairs are numbers, matching the deployed API's raw serialization. -Source: atomicmarket-sdk (main, 278bdfa) src/API/Explorer/index.ts (`getRoyaltyConfig` 416-to-null, `getRoyaltyTemplateRules`, `getRoyaltyAttributeRules`), src/API/Explorer/Objects.ts (`IRoyaltyConfig`, `IRoyaltyTemplateRule`, `IRoyaltyAttributeRule`); live reads against `https://test.wax.api.atomicassets.io`, and a mainnet 404 probe of `/atomicmarket/v1/royalties/` +Source: atomicmarket-sdk (v2.4.1, 437300b) src/API/Explorer/index.ts:148-168 (`getRoyaltyConfig` 416-to-null, `getRoyaltyTemplateRules`, `getRoyaltyAttributeRules`), src/API/Explorer/Objects.ts:168-220 (`IRoyaltyConfig`, `IRoyaltyTemplateRule`, `IRoyaltyAttributeRule`); live reads against `https://test.wax.api.atomicassets.io`, and a live mainnet probe of `https://wax.api.atomicassets.io/atomicmarket/v1/royalties/pixeltycoons` returning HTTP 416 + +### The config and rule row types carry required identity and timestamp fields + +`IRoyaltyConfig`, `IRoyaltyTemplateRule`, and `IRoyaltyAttributeRule` each declare `market_contract` and `collection_name` alongside `updated_at_block`, `updated_at_time`, `created_at_block`, and `created_at_time`, and `IRoyaltyAttributeRule` also declares `lookup_hash`, the hex-encoded sha256 of the attribute the rule matches and the same digest the contract looks the rule up by. Reading a response is unaffected, because the deployed API already serves those columns. Code that builds one of these rows by hand, a test fixture most often, has to supply them: they are required fields, not optional ones. + +Source: atomicmarket-sdk (v2.4.1, 437300b) src/API/Explorer/Objects.ts:177-189 (`IRoyaltyConfig`), src/API/Explorer/Objects.ts:191-200 (`IRoyaltyTemplateRule`), src/API/Explorer/Objects.ts:202-220 (`IRoyaltyAttributeRule` and `lookup_hash`); live reads of `/atomicmarket/v1/royalties/royaltycol11`, `/templates`, and `/attributes` on `https://test.wax.api.atomicassets.io`, each response carrying all of these fields + +### The settled payout ledger + +`getRoyaltyPayouts(options, page, limit)` reads `/v1/royalties/payouts`, the indexer's record of every royalty the contract settled, one row for each entry in a settlement log's payout vector and keyed by that log's `log_global_sequence` plus the entry's `payout_index`. `countRoyaltyPayouts(options)` counts the same set, and `getRoyaltyAccount(account, options)` returns what one account has been paid, one row per token symbol. The route answers newest first. Reading the payouts is what replaces paging the sale, auction, and buyoffer logs and reassembling the splits by hand. + +`IRoyaltyPayout` and `IRoyaltyAccountTotal` both extend `IMarketToken`, so every row carries its own `token_symbol`, `token_precision`, and `token_contract`. `amount` is raw token units and has to be read against the precision on that same row: `5000000` at precision 8 is `0.05000000 WAX`. On an account total, `payout_count` is a decimal string too, because the API serves a SQL count as a string. + +Two fields carry the shape of the row. `category` names the rule that paid, one of `founders`, `template`, `attribute`, or `dust`, and it decides which linkage the row holds: a template payout sets `template_id`, an attribute payout sets `rule_id`, and a founders or dust payout sets neither. A dust row is the settlement remainder plus the author fallback, paid to the collection author, and it names no asset either. `listing_type` is one of `unresolved`, `sale`, `auction`, `buyoffer`, or `template_buyoffer`, and `listing_id` is `null` exactly when the type is `unresolved`, which is the row the filler keeps when it cannot trace a settlement back to a listing. Both `listing_type` and `category` read `null` when the stored value falls outside the vocabulary this SDK serves, so match on the enum values and treat `null` as unknown rather than as absent. + +Filters travel as `RoyaltyPayoutApiParams`: `recipient`, `collection_name`, `asset_id`, `symbol`, `listing_type`, `listing_id`, and `category`, each taking one value or several joined with commas, plus `sort` (`created` or `amount`), `order`, the date window, and a primary boundary (`ids`, `lower_bound`, `upper_bound`) that ranges over `log_global_sequence`. `RoyaltyAccountApiParams` is narrower on purpose: `collection_name`, `symbol`, and the date window alone, because that route groups its rows by token symbol and has no primary column left to bound. `RoyaltyListingType`, `RoyaltyPayoutCategory`, and `RoyaltyPayoutSort` ship as runtime values for those filter strings. + +Neither payout reader maps a status to `null`; `getRoyaltyConfig` remains the only getter that does. An empty ledger is an empty result rather than an absent one, which is what a chain still running AtomicMarket V1 returns: it logs no payouts, so WAX mainnet answers the list with `[]` and the count with `0`. + +Live reads, the first three against the WAX testnet deployment and the last against WAX mainnet: + +```js +await test.getRoyaltyPayouts({ collection_name: 'royaltycol11' }, 1, 2); +// -> [{ market_contract: 'atomicmarket', log_global_sequence: '840330124', payout_index: 0, +// listing_type: 'sale', listing_id: '46890', category: 'attribute', +// collection_name: 'royaltycol11', asset_id: '1099603751717', +// template_id: null, rule_id: '2', recipient: 'jacktestr125', +// amount: '1250000', token_symbol: 'WAX', token_precision: 8, token_contract: 'eosio.token', +// txid: '6df6118a...', created_at_block: '414927681', created_at_time: '1783387748000' }, ... ] + +await test.countRoyaltyPayouts({}); +// -> 20 (the route answers the string "20"; countEndpoint parses it) + +await test.getRoyaltyAccount('jacktestr125'); +// -> [{ token_symbol: 'WAX', token_precision: 8, token_contract: 'eosio.token', +// amount: '76875000', payout_count: '10' }] + +await marketApiForNetwork('wax').getRoyaltyPayouts({}, 1, 2); +// -> [] (WAX mainnet runs AtomicMarket V1, which settles no logged royalties) +``` + +Source: atomicmarket-sdk (v2.4.1, 437300b) src/API/Explorer/index.ts:170-187 (the three readers, the newest-first note, and the empty-ledger case on a V1 chain), src/API/Explorer/Objects.ts:222-250 (`IRoyaltyPayout`, the null rules, `amount` in raw units), src/API/Explorer/Objects.ts:252-258 (`IRoyaltyAccountTotal`), src/API/Explorer/Objects.ts:33-37 (`IMarketToken`), src/API/Explorer/Enums.ts:93-121 (`RoyaltyListingType`, `RoyaltyPayoutCategory`, `RoyaltyPayoutSort`), src/API/Explorer/Params.ts:61-77 (`RoyaltyPayoutApiParams`), src/API/Explorer/Params.ts:79-86 (`RoyaltyAccountApiParams`); live reads of `/atomicmarket/v1/royalties/payouts?collection_name=royaltycol11&limit=2` (200, the two rows above, the second showing a `template` category with `template_id` set and `rule_id` null), `/payouts/_count` (200, `"20"`), and `/accounts/jacktestr125` on `https://test.wax.api.atomicassets.io`, and of the same three routes on `https://wax.api.atomicassets.io` (200, empty list and count `"0"`) + +## Reading the market config, including the delphi pairs + +`getConfig()` returns the contract's live parameters plus the token and symbol-pair registries. The pair entries are what the settlement math below needs, so read the config before deriving a settlement amount rather than hardcoding a precision. + +Live read against the WAX testnet deployment, which runs AtomicMarket V2: + +```js +import { marketApiForNetwork } from '@atomichub/atomicmarket'; + +const test = marketApiForNetwork('wax-testnet'); + +await test.getConfig(); +// -> { atomicmarket_contract: 'atomicmarket', version: '2.0.0', +// maker_market_fee: 0.01, taker_market_fee: 0.01, +// minimum_auction_duration: 120, maximum_auction_duration: 2592000, +// minimum_bid_increase: 0.1, auction_reset_duration: 120, +// supported_tokens: [{ token_contract: 'eosio.token', token_symbol: 'WAX', token_precision: 8 }], +// supported_pairs: [{ listing_symbol: 'USD', settlement_symbol: 'WAX', delphi_pair_name: 'waxpusd', +// invert_delphi_pair: false, +// data: { median: 35, median_precision: 4, base_precision: 8, quote_precision: 2, ... } }] } +``` + +The `version` string is the contract's own, so it reads `1.3.3` against a mainnet host and `2.0.0` against a V2 deployment. Table presence, not the version string, is the authoritative V2 check; see [AtomicAssets V2 upgrade](../atomicassets/v2-upgrade.md). + +Source: atomicmarket-sdk (v2.4.1, 437300b) src/API/Explorer/index.ts:142-144 (`getConfig`), src/API/Explorer/Objects.ts (`IMarketConfig`, `IMarketPair`); live read of `https://test.wax.api.atomicassets.io/atomicmarket/v1/config` -## MarketActionBuilder builds the v2 royalty-config actions +## MarketActionBuilder builds every v2 action, plus five flow composers -The action layer covers the v2 royalty configuration only, not the trade actions (announce, purchase, bid, and the like live in the contract action reference). `MarketActionBuilder(contract)` is synchronous and authorization-free, returning `[{ account, name, data }]`; `MarketActionGenerator(contract)` wraps the same builders as `async` methods that take an `authorization` array first and return `[{ account, name, authorization, data }]`, the shape `@wharfkit` `session.transact({ actions })` accepts. The six actions are `setroyalconf` (founders plus the category split and attribute mode), `settemplroy` (per-template recipients), `setattrroy` (an attribute-match rule), and their deletes `delroyalconf`, `deltemplroy`, `delattrroy`. `AtomicMarketActions` exports every v2 contract action name as string constants for reference. +`MarketActionBuilder(contract)` is synchronous and authorization-free; `MarketActionGenerator(contract)` wraps the same methods as `async` ones that take an `authorization` array first and return `[{ account, name, authorization, data }]`, the shape `@wharfkit` `session.transact({ actions })` accepts. Both classes carry 31 methods: 26 that build one contract action each, and five composers that return a whole flow. -None of these actions carry an `authorized_*` field in `data`: the signer is implicit in the transaction authorization, and adding one is not in the ABI and fails on encode. The builder coerces `uint8`/`uint32`/`int32` fields (weights, splits, `template_id`) through `Number()` so numeric strings are accepted, while `uint64` fields (`rule_id` on `delattrroy`) are forwarded as strings because `Number()` corrupts values above 2^53. +| Family | Builder methods | +| --- | --- | +| Royalty configuration (v2 only) | `setroyalconf`, `settemplroy`, `setattrroy`, `delroyalconf`, `deltemplroy`, `delattrroy` | +| Sale lifecycle | `announcesale`, `assertsale`, `purchasesale`, `cancelsale` | +| Auction lifecycle | `announceauct`, `auctionbid`, `auctclaimbuy`, `auctclaimsel`, `assertauct`, `cancelauct` | +| Buyoffers | `createbuyo`, `declinebuyo`, `cancelbuyo` | +| Template buyoffers | `createtbuyo`, `canceltbuyo` | +| RAM payment | `paysaleram`, `payauctram`, `paybuyoram` | +| Marketplace and balance | `regmarket`, `withdraw` | +| Flow composers | `announceSaleActions`, `purchaseSaleActions`, `announceAuctionActions`, `acceptBuyofferActions`, `fulfillTemplateBuyofferActions` | + +Two contract actions are deliberately absent from that list. `acceptbuyo` and `fulfilltbuyo` have no standalone builder method, because each reads the globally last row of the AtomicAssets offers table rather than an offer id it is handed, so an action built on its own is unsafe. They are reachable only through `acceptBuyofferActions` and `fulfillTemplateBuyofferActions`. `AtomicMarketActions` still exports every v2 contract action name as a string constant, including those two. + +Every builder method returns an array of `{ account, name, data }` objects, even the ones that build a single action. The sibling assets package returns one object rather than an array; see [@atomichub/atomicassets SDK](atomicassets.md#action-building-a-sync-authorization-free-builder-and-an-async-authorization-first-generator) ("Action building"). Spread a market result and push an assets result. + +None of these actions carry an `authorized_*` field in `data`: the signer is implicit in the transaction authorization, and adding one is not in the ABI and fails on encode. The builder coerces `uint8`/`uint32`/`int32` fields (weights, splits, `template_id`) through `Number()` so numeric strings are accepted, while `uint64` fields (`rule_id`, the listing ids, `intended_delphi_median`) are forwarded as strings because `Number()` corrupts values above 2^53. `asset` and `symbol` fields are chain-notation strings (`"1.00000000 WAX"`, `"8,WAX"`) and pass through verbatim. + +Coercion is not validation, and the one numeric field that is checked is `duration` on `announceauct`. `Number()` reads a non-numeric string as `NaN`, which serializes as `null` and reaches a signing library as a field it cannot encode, so the value is refused at the call with an error naming it. The bound is the ABI's, a whole number from 0 to 4294967295; the config's minimum and maximum auction duration are chain state and go unchecked here. ```js import { MarketActionBuilder } from '@atomichub/atomicmarket'; @@ -98,20 +236,96 @@ builder.settemplroy('mycollection', 12345, [{ recipient: 'artistacct', weight: 1 // data: { collection_name: 'mycollection', template_id: 12345, recipients: [{ recipient: 'artistacct', weight: 10000 }] } }] ``` -Source: atomicmarket-sdk (main, 278bdfa) src/Actions/Generator.ts (`MarketActionBuilder`, `MarketActionGenerator`, `AtomicMarketActions`, the numeric-coercion rules); action outputs executed locally +Source: atomicmarket-sdk (v2.4.1, 437300b) src/Actions/Generator.ts:208-734 (`MarketActionBuilder`, 26 action methods and five composers), src/Actions/Generator.ts:731-733 (`_pack` returning an array), src/Actions/Generator.ts:739-944 (`MarketActionGenerator`, `_authorize`), src/Actions/Generator.ts:12-64 (`AtomicMarketActions`), src/Actions/Generator.ts:190-207 (the numeric-coercion rules), src/Actions/Generator.ts:713-729 (`_uint32`), src/Actions/Generator.ts:312-323 (`announceauct` calling it for `duration`); action outputs executed locally + +### The five composers + +Each composer emits a whole flow in the order the contract requires, with the memo literals and the owning contract account filled in. `assets_contract` on the input names the AtomicAssets contract the assets live on, which is `atomicassets` on every current chain. + +| Composer | Emits, in order | Refuses | +| --- | --- | --- | +| `announceSaleActions(input)` | `announcesale`, then the AtomicAssets `createoffer` with memo `sale` | nothing; every remaining condition is chain state | +| `purchaseSaleActions(input)` | `assertsale`, the settlement token's `transfer` with memo `deposit`, then `purchasesale` | a bundle `asset_ids`, and the settlement-quantity mismatches below | +| `announceAuctionActions(input)` | `announceauct`, then the AtomicAssets `transfer` with memo `auction` | nothing; the transfer must follow the announce, and that order is fixed here | +| `acceptBuyofferActions(input)` | the AtomicAssets `createoffer` with memo `buyoffer`, then `acceptbuyo` | a bundle `asset_ids` | +| `fulfillTemplateBuyofferActions(input)` | the AtomicAssets `createoffer` with memo `tbuyoffer`, then `fulfilltbuyo` | nothing; a template buyoffer names one asset by construction | + +The two offer-consuming composers carry a placement rule the caller has to respect: the market action reads the globally last created row of the AtomicAssets offers table, so the offer must be created in the same transaction immediately before it, and no other `createoffer` may run in between. Actions appended after the market action are safe, the inline `acceptoffer` having consumed the row by then. Neither composer accepts the offer itself, because the market contract sends that `acceptoffer` inline and a pre-accepted offer is already gone from the table. + +Source: atomicmarket-sdk (v2.4.1, 437300b) src/Actions/Generator.ts:604-613 (`announceSaleActions`), src/Actions/Generator.ts:493-592 (`purchaseSaleActions`), src/Actions/Generator.ts:630-639 (`announceAuctionActions`), src/Actions/Generator.ts:658-688 (`acceptBuyofferActions`), src/Actions/Generator.ts:694-707 (`fulfillTemplateBuyofferActions`), src/Actions/Generator.ts:641-657 (the last-offer placement rule) + +### The two bundle opt-out flags + +`purchaseSaleActions` throws when `asset_ids` carries more than one id, unless `allow_v1_bundle_sale` is set. `acceptBuyofferActions` does the same behind `allow_v1_bundle_buyoffer`. Both guard the one caller error in their family that commits rather than reverting. + +On a purchase, `purchasesale` under V2 returns early for a sale row holding more than one asset: it declines the offer, erases the row, and returns before touching any balance, while `assertsale` has already passed and the deposit has already credited the buyer. The transaction commits with the buyer paid, nothing delivered, and the tokens recoverable only through a separate `withdraw`. On an accept, `acceptbuyo` under V2 refunds the escrowed price and erases the buyoffer row before it ever reads the offers table, leaving the offer the composer created dangling in the AtomicAssets offers table on the recipient's RAM until they cancel it. + +Set the flag only for a chain still running AtomicMarket V1, where bundle rows are ordinary listings that purchase and accept correctly. The lifecycle side of both rules is in [Working with sales](../../guides/sales.md#purchase-a-sale) ("Purchase a sale") and [Buyoffers](../../guides/buyoffers.md#accepting-a-buyoffer) ("Accepting a buyoffer"). + +Source: atomicmarket-sdk (v2.4.1, 437300b) src/Actions/Generator.ts:115-120 (`allow_v1_bundle_sale`), src/Actions/Generator.ts:504-520 (the purchase throw and its reason), src/Actions/Generator.ts:169-174 (`allow_v1_bundle_buyoffer`), src/Actions/Generator.ts:659-675 (the accept throw and its reason) + +### What purchaseSaleActions requires of settlement_quantity + +The composer keys its checks on the contract's own settlement discriminator: whether `listing_price` and `settlement_symbol` name one symbol, precision and code both. A sale listing `30.00 WAX` against `8,WAX` names two symbols and settles through the oracle like any other cross-symbol sale. + +- When the two name different symbols, `settlement_quantity` is required and must be denominated in `settlement_symbol`. Such a sale settles the oracle conversion of its listing price, not the price itself, and `assertsale` pins only the listing terms, so nothing on chain catches a deposit in the wrong symbol or of the wrong size. +- When the two name one symbol, `settlement_quantity` may be omitted, and a supplied one must equal `listing_price` exactly. `intended_delphi_median` must then be `'0'`. + +Both refusals rule out a transaction the chain would take, deliberately: depositing more than the sale costs leaves the surplus as balance, and depositing nothing lets a standing balance pay. Each is legitimate for a caller who means it and indistinguishable from a wrong amount for one who does not, and the composer cannot see a balance to tell them apart. To do either on purpose, assemble the transaction from `assertsale`, your own transfer, and `purchasesale`, which assert nothing. + +Source: atomicmarket-sdk (v2.4.1, 437300b) src/Actions/Generator.ts:106-112 (the `settlement_quantity` contract on the input type), src/Actions/Generator.ts:494-502 (`namesSameSymbol` as the discriminator), src/Actions/Generator.ts:522-549 (the same-symbol branch), src/Actions/Generator.ts:550-576 (the cross-symbol branch), src/Actions/Symbols.ts:80-86 (`namesSameSymbol`), src/Actions/Symbols.ts:30-69 (quantity and symbol parsing) + +## Delphi settlement math: deriveSettlementAmount and formatQuantity + +A delphi sale lists in one symbol and settles in another at the oracle rate, and `assertsale` pins only the listing terms. Nothing on chain asserts the settlement amount the buyer deposits, which makes deriving it the integrator's hardest step and its failure a wrong payment. `deriveSettlementAmount(listingAmount, median, pair)` returns that amount as a raw integer, and `formatQuantity(rawAmount, precision, symbolCode)` renders it as the chain quantity string an `asset` field expects. `DelphiPairSpec` is the flat projection the derivation needs, assembled from a supported pair in `getConfig()`. + +The derivation reproduces what the contract computes rather than what it ought to compute. The contract divides and scales in binary64 and truncates into a `uint64_t`, so its charge is not the exact rational floor; on the WAX/USD pair it lands a raw unit above the exact floor on a minority of listing amounts. Deriving the exact floor instead would leave the deposit a unit short and the purchase would throw, unless a standing balance quietly covered the difference. The arithmetic here is therefore the contract's, numerical sloppiness included. + +Two conditions throw rather than return a number: + +- A pair whose exponent works out negative. The contract builds that exponent in unsigned 64-bit arithmetic, so a negative one wraps past 1.8e19, overflows the power step, and has no defined conversion. There is no settlement amount to reproduce. +- A result at or past 2^64, the width the contract assigns the converted price into. No purchase at that price can land. + +The helpers also reject a non-positive median, a negative `listingAmount` or `rawAmount`, and a precision outside the 0 to 18 the chain allows. + +Worked against the pair the WAX testnet deployment serves (`median: 35`, `median_precision: 4`, `base_precision: 8`, `quote_precision: 2`, `invert_delphi_pair: false`), for a `30.00 USD` listing: + +```js +import { deriveSettlementAmount, formatQuantity } from '@atomichub/atomicmarket'; + +const pair = { median_precision: 4, base_precision: 8, quote_precision: 2, invert_delphi_pair: false }; + +const raw = deriveSettlementAmount(3000n, 35n, pair); +// -> 857142857142n (exponent 4 + 8 - 2 = 10; 3000 / 35 * 1e10, truncated) + +formatQuantity(raw, 8, 'WAX'); +// -> '8571.42857142 WAX' (pass this as settlement_quantity, and '35' as intended_delphi_median) +``` + +Read the median immediately before submitting and pass that exact value as `intended_delphi_median`; the contract scans the oracle's `datapoints` table for a row matching it and throws when none does. The chain-side conversion rule and the datapoint read are in [Working with sales](../../guides/sales.md#delphi-oracle-sales) ("Delphi (oracle) sales"). + +Source: atomicmarket-sdk (v2.4.1, 437300b) src/Actions/Delphi.ts:15-20 (`DelphiPairSpec`), src/Actions/Delphi.ts:27-31 (the 0 to 18 precision bound), src/Actions/Delphi.ts:37 (`UINT64_LIMIT`), src/Actions/Delphi.ts:72-122 (`deriveSettlementAmount`, the exponent, the two throws, the truncation), src/Actions/Delphi.ts:132-153 (`formatQuantity`); the pair values live-read from `https://test.wax.api.atomicassets.io/atomicmarket/v1/config`, the worked figures computed from the pinned formula + +## Typed table rows ship alongside the API types + +`src/Tables.ts` exports the `get_table_rows` shapes for the v2 contract tables, so a chain-side read deserializes into a named type instead of `any`. Field widths follow the on-chain ABI: `uint64` and `name` fields arrive as strings, and `int32`/`uint32`/`uint8`/`float64` fields arrive as numbers. Use these when reading the market's tables directly rather than through the indexer. + +Source: atomicmarket-sdk (v2.4.1, 437300b) src/Tables.ts:1-46 (row interfaces and the field-width rule), src/index.ts:27-28 (root re-export) ## Network factory carries AtomicHub's public hosts `marketApiForNetwork(network, options?)` constructs an `AtomicMarketApi` against AtomicHub's public endpoints for the same `AtomicHubNetwork` keys the assets SDK defines (`wax`, `wax-testnet`, `vaulta`, `xpr`, `xpr-testnet`, `jungle4`), reusing the re-exported `NETWORK_ENDPOINTS`. Any compatible deployment can be passed straight to the `AtomicMarketApi` constructor instead. -Source: atomicmarket-sdk (main, 278bdfa) src/Networks.ts (`marketApiForNetwork`, re-exported `AtomicHubNetwork`/`NETWORK_ENDPOINTS`); `wax` and `wax-testnet` factories verified live +Source: atomicmarket-sdk (v2.4.1, 437300b) src/Networks.ts:12-16 (`marketApiForNetwork`), src/Networks.ts:1-10 (re-exported `AtomicHubNetwork`/`NETWORK_ENDPOINTS`); `wax` and `wax-testnet` factories verified live ## When to use the SDK versus raw HTTP or WharfKit table reads The read-path choice mirrors the AtomicAssets SDK, consistent with `guides/querying-the-api.md`: -- **`AtomicMarketApi` (this SDK)** for typed indexer reads of sales, auctions, buyoffers, marketplaces, and the royalty read layer from JS/TS, with params and response objects typed and failures raised as `ApiError`. -- **Raw HTTP against the AtomicMarket API** outside a JS runtime, or when you want direct control over paging; the endpoints, limit cap, and per-endpoint `state` enums are in [atomicassets-api HTTP API](../api.md). -- **Chain table reads** (`@wharfkit/antelope` `get_table_rows`) for unindexed marketplace state without indexer lag; mind the numeric-key and `show_payer` behaviors in [@wharfkit/antelope client behavior](../wharfkit.md), and that large ids can arrive as strings. +| Read path | When it is right | Caveat | +| --- | --- | --- | +| `AtomicMarketApi` (this SDK) | Typed indexer reads of sales, auctions, buyoffers, marketplaces, the royalty layer, and the settled payout ledger from JS/TS | Inherits the deployment's `limit` cap of 100, and failures arrive as a rejected promise carrying `ApiError`, except a bad path id, which throws a plain `Error` before any request | +| Raw HTTP against the AtomicMarket API | Outside a JS runtime, or when you want direct control over paging | You own the percent-encoding, the state-enum choice, and treating a non-2xx as an error; the endpoints are in [atomicassets-api HTTP API](../api.md) | +| Chain table reads (`@wharfkit/antelope` `get_table_rows`) | Unindexed marketplace state without indexer lag | The numeric-key and `show_payer` behaviors in [@wharfkit/antelope client behavior](../wharfkit.md) apply, and large ids can arrive as strings | List endpoints reached through `AtomicMarketApi` inherit the deployment's `limit` cap of 100; see [atomicassets-api HTTP API](../api.md#list-endpoints-cap-limit-at-100) ("List endpoints cap limit at 100"). diff --git a/validation-log.md b/validation-log.md index 35c7213..b067767 100644 --- a/validation-log.md +++ b/validation-log.md @@ -14,7 +14,8 @@ This log traces how every fact in `reference/` and `guides/` was checked before - `atomicmarket-contract` at `v2.0.0-rc2` - `atomicassets-api` at its current `main` branch state (no release tag; indexer behavior and API surface are read from the running source tree; streaming and rate-limit pages pin `f6419858`) - `atomictools-contract` at commit `d89ce79e4` (the upstream repository has no release tag; the deployed `atomictoolsx` ABI on WAX matches this commit exactly) -- `atomicassets-sdk` at main `80580c5` and `atomicmarket-sdk` at main `278bdfa` (both version 2.0.0) +- `atomicassets-sdk` at tag `v2.1.1`, commit `5c70c62` (published as `@atomichub/atomicassets` 2.1.1) +- `atomicmarket-sdk` at tag `v2.4.1`, commit `437300b` (published as `@atomichub/atomicmarket` 2.4.1) - `@wharfkit/antelope` at `1.1.1` - `@atomichub/vert` at `2.2.0`, commit `a8a4160` @@ -28,13 +29,13 @@ WAX mainnet still runs the V1 `atomicassets` and `atomicmarket` contracts (confi | Page | Primary source (repo + key files) | Verification tier | Notes | | --- | --- | --- | --- | -| `reference/api.md` | `atomicassets-api`: `src/api/server.ts`, `src/api/namespaces/*/openapi.ts`; live probes of `wax.api.atomicassets.io` | both | The Swagger-UI section cites both the server routing source and live probes of `/docs`, `/openapi.json`, `/docs/swagger-ui-init.js` in one `Source:` line. The pagination-cap section is a live-observed fact against the hosted deployment. The buyoffer-lifecycle-states section describes indexer state-machine behavior with no dedicated `Source:` line in this page (see `reference/atomicassets-api.md` for the indexer side). The rate-limits section is live-observed (`ratelimit-limit: 240`, `ratelimit-policy: 240;w=60`, plus the legacy `x-ratelimit-*` set) and cross-cited to `src/api/server.ts` and the config schema. | +| `reference/api.md` | `atomicassets-api`: `src/api/server.ts`, `src/api/namespaces/*/openapi.ts`; live probes of `wax.api.atomicassets.io` and `test.wax.api.atomicassets.io` | both | The Swagger-UI section cites both the server routing source and live probes of `/docs`, `/openapi.json`, `/docs/swagger-ui-init.js` in one `Source:` line. The pagination-cap section is a live-observed fact against the hosted deployment. The buyoffer-lifecycle-states section describes indexer state-machine behavior with no dedicated `Source:` line in this page (see `reference/atomicassets-api.md` for the indexer side). The rate-limits section is live-observed (`ratelimit-limit: 240`, `ratelimit-policy: 240;w=60`, plus the legacy `x-ratelimit-*` set) and cross-cited to `src/api/server.ts` and the config schema. The two-sales-routes and royalty-416 sections are live-chain only, each carrying its own `Source:` line naming the probes: both sales routes and their `_count` siblings answer 200 with equal counts, the served OpenAPI document carries `/atomicmarket/v2/sales` and no `/atomicmarket/v1/sales`, and the royalty route answers 416 on mainnet for every collection and on testnet only for a collection with no config. The claim that `/v2/sales` omits waiting sales is not validated and is held in `learning/api.md` rather than stated here. | | `reference/api-streaming.md` | `atomicassets-api` (main, `f6419858`): `src/api/server.ts`, `src/api/utils.ts`, `src/api/notification.ts`, `src/api/namespaces/*/routes/*.ts`; live Socket.IO probe of `wss://wax.api.atomicassets.io` | both | Namespace names, WebSocket-only transport, and connectivity are live-confirmed (five namespaces handshook). The event catalog, payload shapes, room semantics, transfers-on-offers quirk, and the unwired template-buyoffer handler are source-read; no socket events were observed in the probe windows. | | `reference/atomictools/actions.md` | `atomictools-contract` (commit `d89ce79e4`): `src/link.cpp`, `src/auth.cpp`, `include/atomictoolsx.hpp`; live `get_abi` diff against `atomictoolsx` on WAX mainnet | both | Every action cites header and implementation line ranges. The full action/table list was diffed against the deployed ABI and matches the pinned source exactly; `config.version` reads `1.0.0` live. | | `reference/atomictools/tables.md` | `atomictools-contract` (commit `d89ce79e4`): `include/atomictoolsx.hpp`, `src/link.cpp`; live `get_table_rows` against `wax.greymass.com` | both | Two tables (`links`, `config`), each with its own citation. Row shapes and the `assetidshash` secondary index confirmed by live primary- and secondary-index reads. | | `reference/media.md` | Live reads of `wax.api.atomicassets.io` (templates, schemas, collections across alien.worlds, farmersworld, gpk.topps, official.wax, kogsofficial) and a public IPFS gateway (`ipfs.io`); type/layer facts drawn from `reference/atomicassets/serialization.md`, `custom-types.md`, `data-precedence.md` | both | Field-name conventions and value shapes (bare CIDv0/CIDv1, CID-plus-path) are live-observed across five major WAX collections; the media FORMAT-type convention (`image`/`string`, not `ipfs`) is live-read from schema `format`; gateway resolution is confirmed by a live `ipfs.io` fetch returning `image/webp` with WebP magic bytes. No dedicated `Source:` line consolidates the page; each section carries its own live-read citation. | -| `reference/sdk/atomicassets.md` | `atomicassets-sdk` (main, `80580c5`): `src/index.ts`, `src/API/Explorer/index.ts`, `src/API/Rpc/index.ts`, `src/Actions/Generator.ts`, `src/Serialization/index.ts`, `src/Schema/index.ts`, `src/Networks.ts`; live reads of `wax.api.atomicassets.io` | both | Getter surface, serialization split, action shapes, and error types read from the 2.0.0 source. ExplorerApi reads, a serialization round-trip against a live schema format, the 8-arg `mintasset` output, and the network factories were executed against the built SDK. The zero-runtime-deps fact is from `package.json`. No release tag; pinned to the main HEAD commit. | -| `reference/sdk/atomicmarket.md` | `atomicmarket-sdk` (main, `278bdfa`): `src/API/Explorer/index.ts`, `src/Actions/Generator.ts`, `src/API/Explorer/Objects.ts`, `src/Networks.ts`; live reads of `wax.api.atomicassets.io` and `test.wax.api.atomicassets.io` | both | Listing getters, `getConfig`, and royalty-action builders read from the 2.0.0 source. Sales and config reads executed live on WAX mainnet; the V2 royalty read layer executed against WAX testnet, where `getRoyaltyConfig` returns a real config and the 416-to-null mapping was confirmed, while the mainnet reference deployment returns 404 for `/v1/royalties/*` (V1). Single runtime dependency `@atomichub/atomicassets`. | +| `reference/sdk/atomicassets.md` | `atomicassets-sdk` (`v2.1.1`, `5c70c62`): `src/index.ts`, `src/API/Explorer/index.ts`, `src/API/Rpc/index.ts`, `src/Actions/Generator.ts`, `src/Serialization/index.ts`, `src/Schema/index.ts`, `src/Networks.ts`, `package.json`, `test/explorer-url.test.ts`; live reads of `wax.api.atomicassets.io` | both | The getter-and-route table, the serialization split, action shapes, and error types are read from the 2.1.1 source, each section citing file and line. Source-read at 2.1.1: the lazy `action` getter (construction starts no request), percent-encoding of path segments and of both sides of every query pair, the empty-and-dot-segment guard added in 2.1.1 (its two throw messages, the sixteen guarded getters, the plain `Error` rather than an `ApiError`, and that nothing is sent, all read from `encodeSegment` and the paired test), the numeric ABI-type guards and the fields they cover, the `backasset` and `tokens_to_back` deprecation, and the one-object-versus-array asymmetry against the market builder. The `getTemplateStats` row now reads `(collection, id)`, correcting a `name` the 2.1.1 signature renamed. ExplorerApi reads, a serialization round-trip against a live schema format, the 8-arg `mintasset` output, and the network factories were executed against the built SDK at the earlier 2.0.0 pin and the affected signatures re-read at 2.1.1. The zero-runtime-deps and `sideEffects` facts are from `package.json`. | +| `reference/sdk/atomicmarket.md` | `atomicmarket-sdk` (`v2.4.1`, `437300b`): `src/index.ts`, `src/API/Explorer/index.ts`, `src/API/Explorer/Objects.ts`, `src/API/Explorer/Enums.ts`, `src/API/Explorer/Params.ts`, `src/Actions/Generator.ts`, `src/Actions/Delphi.ts`, `src/Actions/Symbols.ts`, `src/Tables.ts`, `src/Networks.ts`, `package.json`, `test/path-segments.test.ts`; live reads of `wax.api.atomicassets.io` and `test.wax.api.atomicassets.io` | both | The read-surface table, the 31-method action surface (26 actions plus five composers), the composer contracts, and the delphi settlement math are read from the 2.4.1 source, each section citing file and line. Source-read at 2.4.1: the empty-and-dot-segment guard (its two throw messages, the thirteen guarded readers, the plain `Error` that travels out of `getRoyaltyConfig` because only a 416 `ApiError` maps to `null`), and the payout filter surfaces `RoyaltyPayoutApiParams` and `RoyaltyAccountApiParams`. Live-chain: `/atomicmarket/v2/sales` and its `_count` answer 200 on WAX mainnet; the mainnet royalty route answers HTTP 416 with `Royalty config not found`, which corrects the 404 this page previously claimed and inverts the guard advice, since `getRoyaltyConfig` maps 416 to `null`; the WAX testnet royalty reads and the testnet `getConfig` sample (contract `version: 2.0.0`, the `waxpusd` pair) were read live, and mainnet `getConfig` reads `1.3.3`. Live-chain for the 2.4.0 payout ledger: the testnet `/royalties/payouts`, `/payouts/_count`, and `/accounts/{account}` routes answer 200 with rows matching `IRoyaltyPayout` and `IRoyaltyAccountTotal` field for field, the `_count` value arrives as the string `"20"`, the sampled rows confirm the category-to-linkage rule, and the same three routes on WAX mainnet answer 200 with an empty list and a zero count, which is the V1-chain case. The added `market_contract`, `collection_name`, timestamp, and `lookup_hash` fields on the config and rule rows are live-read from the testnet royalty routes as well as declared in `Objects.ts`. The worked `deriveSettlementAmount` figures are computed from the pinned formula against that live pair, not observed on chain. | | `reference/atomicassets-api.md` | `atomicassets-api`: `package.json`, `src/api/server.ts` | source-read | Cites repo metadata and the documentation-server source; no live probe cited. | | `reference/atomicassets/actions.md` | `atomicassets-contract` (v2.0.0-rc4): `src/atomicassets.cpp`, `include/atomicassets.hpp` | source-read | Every action cites specific header and implementation line ranges. | | `reference/atomicassets/backing-tokens.md` | `atomicassets-contract` (v2.0.0-rc4): `src/atomicassets.cpp`, `include/atomicassets.hpp` | source-read | Cites `announcedepo`, `withdraw`, `addconftoken`, `burnasset`, and the V2 `backasset` abort by line range. | @@ -54,15 +55,15 @@ WAX mainnet still runs the V1 `atomicassets` and `atomicmarket` contracts (confi | `reference/chain.md` | Live `nodeos`/WAX RPC behavior (`/v1/chain/get_account`); nodeos `chain_plugin.cpp` referenced for the error-message format | live-chain | No dedicated `Source:` line in this page. The error code and HTTP behavior are a live-RPC fact; the page also names the nodeos source file that emits the message text, which is not independently re-verified here. | | `reference/contract-releases.md` | `atomicassets-contract` / `atomicmarket-contract`: `Makefile`, `scripts/patch-abi.py`, CI release workflow | source-read | No dedicated `Source:` line and no version pin in this page's frontmatter (build tooling applies across releases, not to one tag). Resource-usage figures (NET/CPU for `setcode`) read as measured observations rather than a cited source or a live probe; treat those magnitudes as approximate. | | `reference/wharfkit.md` | `@wharfkit/antelope` client library source, pinned to `1.1.1` | source-read | No dedicated `Source:` line; each fact names the library behavior and, for one, an `@attention` note in the library's own source. Re-check on any `@wharfkit/antelope` upgrade, as the page itself states. | -| `guides/asset-lifecycle.md` | `atomicassets-contract` (v2.0.0-rc4): `src/atomicassets.cpp`, `include/atomicassets.hpp` | source-read | Creator-flow walkthrough; every step cites the underlying action's source range. | -| `guides/auctions.md` | `atomicmarket-contract` (v2.0.0-rc2): `src/atomicmarket.cpp`; live `get_table_rows` curl example against `wax.greymass.com` | both | Lifecycle steps cite contract source; one section shows a live `get_table_rows` curl call to illustrate reading auction state. | -| `guides/buyoffers.md` | `atomicmarket-contract` (v2.0.0-rc2): `src/atomicmarket.cpp` | source-read | Every action (create, accept, decline, cancel, template variants) cites its source range. | +| `guides/asset-lifecycle.md` | `atomicassets-contract` (v2.0.0-rc4): `src/atomicassets.cpp`, `include/atomicassets.hpp`; `atomicassets-sdk` (`v2.1.1`, `5c70c62`): `src/Actions/Generator.ts` | source-read | Creator-flow walkthrough; every step cites the underlying action's source range. The SDK mint example and the numeric-guard note are source-read from the 2.1.1 builder, not executed: the guarded fields, the `-1` sentinel, and the single-object return shape are read from `Generator.ts` by line. | +| `guides/auctions.md` | `atomicmarket-contract` (v2.0.0-rc2): `src/atomicmarket.cpp`; `atomicmarket-sdk` (`v2.4.1`, `437300b`): `src/Actions/Generator.ts`; live `get_table_rows` curl example against `wax.greymass.com` | both | Lifecycle steps cite contract source; one section shows a live `get_table_rows` curl call to illustrate reading auction state. The composer section is source-read from the 2.4.1 builder: the announce-then-transfer order, the `auction` memo literal, and the `duration` uint32 check are read by line. | +| `guides/buyoffers.md` | `atomicmarket-contract` (v2.0.0-rc2): `src/atomicmarket.cpp`; `atomicmarket-sdk` (`v2.4.1`, `437300b`): `src/Actions/Generator.ts` | source-read | Every action (create, accept, decline, cancel, template variants) cites its source range. The two composer sections are source-read from the 2.4.1 builder: that `acceptbuyo` and `fulfilltbuyo` have no standalone builder method, the emitted order and memo literals, and the `allow_v1_bundle_buyoffer` throw are read by line. | | `guides/deposits.md` | `atomicmarket-contract` (v2.0.0-rc2): `src/atomicmarket.cpp`, `include/atomicmarket.hpp` | source-read | Balance ledger mechanics cite `internal_add_balance`, `internal_decrease_balance`, and `withdraw` by range. | | `guides/links.md` | `atomictools-contract` (commit `d89ce79e4`): `src/link.cpp`; `atomicassets-api` `src/filler/handlers/atomictools`; live `get_table_rows` and `wax.api.atomicassets.io/atomictools/v1` probes | both | Lifecycle steps cite contract source; the funding-by-transfer, signature claim, and cancel flows are source-read. The chain-read curl examples and the hosted-API state table (LinkState 0-3) are live-confirmed, the enum cross-checked against the indexer handler. No transactions were broadcast. | | `guides/notification-integration.md` | `atomicassets-contract` (v2.0.0-rc4): `src/atomicassets.cpp`, `include/atomicassets.hpp` | source-read | Handler wiring, exact ABI signatures, and the notification emission sites are cited by line range against the pinned contract. Two claims about the Antelope runtime (what authorization a notification handler sees, and which payer a notification context may bill for RAM) are not properties of the pinned source; the page states them as safe practice, anchored to the contract's own `receive_token_transfer` (`get_first_receiver` authentication, `same_payer` writes) rather than asserting a runtime rule. | | `guides/offers.md` | `atomicassets-contract` (v2.0.0-rc4): `src/atomicassets.cpp` | source-read | The underlying `createoffer`/`acceptoffer` primitive and how AtomicMarket sales build on it, all cited to contract source. | -| `guides/querying-the-api.md` | Synthesizes `reference/api.md`, `reference/wharfkit.md`, `reference/chain.md`, `reference/atomicmarket/v2-changes.md`; live curl examples against `wax.api.atomicassets.io` | both | This page has no `Source:` line of its own; it links back to the reference page carrying each cited fact and shows live curl output for the pagination and lifecycle-state examples. | -| `guides/sales.md` | `atomicmarket-contract` (v2.0.0-rc2): `src/atomicmarket.cpp`; live `get_table_rows` curl examples against `wax.greymass.com` | both | Lifecycle steps (`announcesale`, `purchasesale`, `cancelsale`, Delphi pricing) cite contract source; three sections show live `get_table_rows` curl calls. | +| `guides/querying-the-api.md` | Synthesizes `reference/api.md`, `reference/wharfkit.md`, `reference/chain.md`, `reference/atomicmarket/v2-changes.md`, `reference/sdk/atomicassets.md`; live curl examples against `wax.api.atomicassets.io` | both | This page has no `Source:` line of its own; it links back to the reference page carrying each cited fact and shows live curl output for the pagination and lifecycle-state examples. The percent-encoding section is source-read from both SDK Explorer clients and live-confirmed for the colon case: `data:text.rarity=Common` and `data%3Atext.rarity=Common` return the same rows from the WAX deployment. The testnet-host guidance is promoted to its own section with a host table and no new facts. | +| `guides/sales.md` | `atomicmarket-contract` (v2.0.0-rc2): `src/atomicmarket.cpp`; `atomicmarket-sdk` (`v2.4.1`, `437300b`): `src/Actions/Generator.ts`, `src/Actions/Delphi.ts`; live `get_table_rows` curl examples against `wax.greymass.com` | both | Lifecycle steps (`announcesale`, `purchasesale`, `cancelsale`, Delphi pricing) cite contract source; three sections show live `get_table_rows` curl calls. The two composer sections and the `settlement_quantity` rules are source-read from the 2.4.1 builder: the emitted action order, the `sale` and `deposit` memo literals, the `allow_v1_bundle_sale` throw, and both settlement branches are read by line. No transaction was broadcast. | | `guides/testing-with-vert.md` | `@atomichub/vert` (2.2.0, commit `a8a4160`): `src/antelope/blockchain.ts`, `src/antelope/vm.ts`, `src/antelope/table.ts`, `examples/` | source-read | Cites the emulator, VM host functions, and table store by line range. Beyond the source read, every code snippet was executed: the library's own suite runs 35 passing on Node 22, the `fixtures` and `timer` examples pass end-to-end, and purpose-built probe contracts confirmed the notification/inline transaction-context limit, the `set`-injected `modify` abort, the permission-exists throw, and the `createContract` load timing. Tiered source-read rather than live-chain because in-process WASM emulation is not a chain read. | ## Tier distribution @@ -71,4 +72,4 @@ WAX mainnet still runs the V1 `atomicassets` and `atomicmarket` contracts (confi ## Pages with an ambiguous tier signal -Three reference pages carry no dedicated `Source:` line at all and were tiered from their content rather than a citation: `reference/chain.md` (tiered live-chain: it describes a live-RPC error code, though it also names a nodeos source file for the message format), `reference/contract-releases.md` (tiered source-read: it describes build-script and CI behavior with no version pin in its frontmatter and no measured-figure citation), and `reference/wharfkit.md` (tiered source-read: it describes a pinned library version's behavior without a formal citation line). `reference/api.md` has a single `Source:` line that names both contract-adjacent source files and live probes together for one section, while its other two sections carry no line-level citation at all; it is tiered `both` on the strength of that mixed citation and the pagination section's live-observed nature, but a reader relying on citation format alone could reasonably read it as `live-chain` only. +Three reference pages carry no dedicated `Source:` line at all and were tiered from their content rather than a citation: `reference/chain.md` (tiered live-chain: it describes a live-RPC error code, though it also names a nodeos source file for the message format), `reference/contract-releases.md` (tiered source-read: it describes build-script and CI behavior with no version pin in its frontmatter and no measured-figure citation), and `reference/wharfkit.md` (tiered source-read: it describes a pinned library version's behavior without a formal citation line). `reference/api.md` is mixed rather than ambiguous: its Swagger-UI section names contract-adjacent source files and live probes together in one `Source:` line, the two sales-route and royalty sections each carry their own live-probe line, and the pagination-cap and buyoffer-lifecycle sections carry none. It is tiered `both` on the strength of the cited sections, and a reader relying on citation format alone would read the two uncited sections as unsupported rather than as the wrong tier.