From b4bd546df9adb9e3623eea493fc8380321b6f757 Mon Sep 17 00:00:00 2001 From: Rob Konsdorf Date: Thu, 16 Jul 2026 11:29:02 -0400 Subject: [PATCH] docs: cover the sdk, vert testing, and thin protocol surfaces R1 documented the contracts and API but left the client side untaught: the official SDKs had no page at all, vert was referenced but never explained, and atomictools, media conventions, the socket surface, and rate limits were thin or absent. Nine new pages close those gaps, each validated against pinned source or live reads per the repo's process, with executable snippets that were actually run. Two live-verified corrections ride along: the API's mediatype synthesis exists only on the 2.0.0 line, and template buyoffers broadcast no socket events at the pinned commit. --- AGENTS.md | 11 +- README.md | 11 +- guides/links.md | 246 +++++++++++++++++++++++++++++ guides/notification-integration.md | 158 ++++++++++++++++++ guides/testing-with-vert.md | 220 ++++++++++++++++++++++++++ reference/api-streaming.md | 95 +++++++++++ reference/api.md | 12 +- reference/atomicassets/tables.md | 2 +- reference/atomictools/actions.md | 144 +++++++++++++++++ reference/atomictools/tables.md | 78 +++++++++ reference/media.md | 90 +++++++++++ reference/sdk/atomicassets.md | 128 +++++++++++++++ reference/sdk/atomicmarket.md | 117 ++++++++++++++ validation-log.md | 20 ++- 14 files changed, 1321 insertions(+), 11 deletions(-) create mode 100644 guides/links.md create mode 100644 guides/notification-integration.md create mode 100644 guides/testing-with-vert.md create mode 100644 reference/api-streaming.md create mode 100644 reference/atomictools/actions.md create mode 100644 reference/atomictools/tables.md create mode 100644 reference/media.md create mode 100644 reference/sdk/atomicassets.md create mode 100644 reference/sdk/atomicmarket.md diff --git a/AGENTS.md b/AGENTS.md index 40558b5..20a79cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,10 +27,16 @@ Start here. Identify the domain the task touches, then read the matching file be | AtomicAssets notifications: collection notify accounts, require_recipient targets per action | `reference/atomicassets/notifications.md` | | AtomicAssets backing tokens: deposit/backasset flow, balances table, burnasset recovery | `reference/atomicassets/backing-tokens.md` | | AtomicAssets V2 upgrade and indexer/chain compatibility | `reference/atomicassets/v2-upgrade.md` | -| Calling atomicassets-api HTTP endpoints: pagination limits, buyoffer lifecycle states | `reference/api.md` | +| AtomicTools claim-links action reference: announce, fund, claim, cancel, the signature claim mechanism | `reference/atomictools/actions.md` | +| AtomicTools tables (links, config): schema, scope, the assetidshash index | `reference/atomictools/tables.md` | +| NFT media: IPFS references, field-name conventions (img/backimg/video), bare-CID vs URL parsing, gateway resolution, V2 media-type descriptors | `reference/media.md` | +| Calling atomicassets-api HTTP endpoints: pagination limits, buyoffer lifecycle states, rate limits | `reference/api.md` | +| atomicassets-api realtime/streaming: Socket.IO namespaces, events, room subscription, socket auth and limits | `reference/api-streaming.md` | | Running or extending the atomicassets-api indexer (also known as eosio-contract-api): handler configuration, drain gating, data freshness, migrations, CI | `reference/atomicassets-api.md` | | Building or deploying contract releases: ABI artifacts, setcode resource needs, deploy permissions | `reference/contract-releases.md` | | @wharfkit/antelope client behavior: table reads, authority checks, show_payer, eosjs migration | `reference/wharfkit.md` | +| @atomichub/atomicassets JS/TS SDK: ExplorerApi/RpcApi reads, attribute serialization, V2 action building | `reference/sdk/atomicassets.md` | +| @atomichub/atomicmarket JS/TS SDK: sales/auctions/buyoffers reads, V2 royalty read layer, royalty-config action building | `reference/sdk/atomicmarket.md` | | Chain RPC error semantics: account-not-found and error classification | `reference/chain.md` | | AtomicAssets creator flow end to end: create collection, schema, template, mint, transfer, burn | `guides/asset-lifecycle.md` | | AtomicAssets offers: two-sided trade offers, accept, decline, cancel | `guides/offers.md` | @@ -38,6 +44,9 @@ Start here. Identify the domain the task touches, then read the matching file be | AtomicMarket auctions end to end: announce, bid, claim, cancel | `guides/auctions.md` | | AtomicMarket buyoffers and template buyoffers: create, accept, decline, cancel | `guides/buyoffers.md` | | AtomicMarket deposits: balances, deposit and withdraw, which actions consume balance | `guides/deposits.md` | +| AtomicTools claim-links end to end: generate link key, announce, fund by transfer, claim by signature, cancel, reading links | `guides/links.md` | +| Building a contract that reacts to AtomicAssets notifications: on_notify wiring, exact-match signatures, same-transaction safety | `guides/notification-integration.md` | +| Testing Antelope contracts in-process with @atomichub/vert: setup, per-chain host functions, time and auth control, known limits | `guides/testing-with-vert.md` | | Querying the API and reading chain tables end to end | `guides/querying-the-api.md` | ## How to use this repo as an agent diff --git a/README.md b/README.md index b5ac6ca..c4aaa77 100644 --- a/README.md +++ b/README.md @@ -8,17 +8,20 @@ Every statement in this repository was validated against contract or indexer sou | Area | Behavior documented | | --- | --- | -| Contracts | AtomicAssets and AtomicMarket: listing semantics, fee application, royalty logging, upgrade compatibility, and defensive guards | +| Contracts | AtomicAssets, AtomicMarket, and AtomicTools (claim links): listing semantics, fee application, royalty logging, the claim-link escrow-and-signature flow, upgrade compatibility, and defensive guards | | Indexer | atomicassets-api (formerly eosio-contract-api) operation: handler dependencies, drain gating, data freshness, migrations, and CI | -| API | The hosted atomicassets-api endpoints (e.g. wax.api.atomicassets.io): pagination limits, lifecycle states, and query semantics | +| API | The hosted atomicassets-api endpoints (e.g. wax.api.atomicassets.io): pagination limits, lifecycle states, query semantics, rate limits, and the Socket.IO realtime surface | | Chain and clients | nodeos RPC behavior, error semantics, and @wharfkit/antelope client-library behavior | +| SDKs | The official @atomichub/atomicassets and @atomichub/atomicmarket JavaScript/TypeScript clients: typed API and chain-table reads, attribute serialization, and V2 action building | +| Media | How NFT media is referenced on chain (bare IPFS CIDs under de-facto img/backimg/video fields), value-shape parsing, and gateway resolution | +| Testing | Running compiled Antelope contracts in-process with @atomichub/vert (VeRT): action calls, table reads, time control, the per-chain host-function gate, and the emulation's limits | ## Organization | Directory | Contents | | --- | --- | -| `reference/` | Facts: `atomicassets/` and `atomicmarket/` directories with per-topic contract pages (structure, actions, tables, fees, serialization), plus one file each for the indexer, API, chain, and client libraries | -| `guides/` | End-to-end workflows: asset lifecycle, offers, sales, auctions, buyoffers, deposits, and querying the API | +| `reference/` | Facts: `atomicassets/`, `atomicmarket/`, `atomictools/`, and `sdk/` directories with per-topic pages (structure, actions, tables, fees, serialization, SDK surfaces), plus one file each for the indexer, API, API streaming, media conventions, chain, and client libraries | +| `guides/` | End-to-end workflows: asset lifecycle, offers, sales, auctions, buyoffers, deposits, claim links, notification integration, contract testing with VeRT, and querying the API | | `skills/` | Agent skills that route coding agents to the relevant reference material | Agents should start at `AGENTS.md`; humans can browse `reference/` directly. diff --git a/guides/links.md b/guides/links.md new file mode 100644 index 0000000..3d0dea4 --- /dev/null +++ b/guides/links.md @@ -0,0 +1,246 @@ +--- +scope: The atomictools claim-link workflow end to end - announce, fund, claim by signature, cancel, and reading links via chain and the hosted API +depends-on: [reference/atomictools/actions.md, reference/atomictools/tables.md, reference/atomicassets/actions.md] +key-modules: ["atomictools-contract (commit d89ce79e4): src/link.cpp, include/atomictoolsx.hpp", "atomicassets-api: src/filler/handlers/atomictools"] +--- + +# Links: the atomictools claim-link flow + +A claim link (or "claimlink") lets someone hand a set of AtomicAssets NFTs to a recipient who does not yet have an account name in hand: the sender escrows the assets against an off-chain key pair, and whoever receives the private key can claim the assets to any account they control. It is how "here is a link, open it to receive these NFTs" flows are built on Antelope chains. The contract is deployed as account `atomictoolsx` on WAX and under the same name on other chains; the `config.atomicassets_account` it escrows through is `atomicassets`. + +The contract has no release tags. Source citations pin commit `d89ce79e4` of `pinknetworkx/atomictools-contract`. This guide shows each write as plain JSON action data first, then the same call through `@wharfkit/session`'s `session.transact()`; it does not broadcast any transaction. The read examples are live `get_table_rows` and hosted-API calls that were run read-only against `wax.greymass.com` and `wax.api.atomicassets.io`. Asset ids and the `link_counter` exceed the JavaScript safe-integer range and must be passed as strings; see `reference/wharfkit.md` and `reference/atomicmarket/v2-changes.md` ("Large integers serialize as strings"). + +## The shape of the flow + +1. The sender generates a key pair off chain. This is the "link key" and is unrelated to any account's keys. +2. `announcelink` records the link with the public key, the asset ids, and a memo. Nothing moves yet. +3. The sender funds the link with a plain AtomicAssets `transfer` of those exact assets to `atomictoolsx`, memo `"link"`. The contract's notification handler marks the link funded. +4. The private key is shared with the recipient out of band (typically embedded in a URL). If it rides in a URL, put it in the fragment (`#...`), never the query string or path: fragments stay in the browser, while query and path components leak through server access logs, referrer headers, and analytics, and any of those exposures hands the assets to whoever reads the log. +5. The recipient calls `claimlink` from their own account, signing their account name with the private key. The contract verifies the signature and sends the assets to them. +6. If no one claims, the sender calls `cancellink` to pull the assets back. + +Steps 2 and 3 are separate transactions and separate authorizations; the link exists in a `WAITING` state between them. + +## Step 1: generate the link key off chain + +The link key is an ordinary Antelope key pair created by the sender's app, held nowhere on chain except its public half inside the link. The private half is the bearer secret: anyone who obtains it can claim. Generate it however the app already builds keys (for example `PrivateKey.generate('K1')` from `@wharfkit/antelope`), keep the private key to place in the shareable link, and pass the public key to `announcelink`. + +There is no on-chain step here; it is stated so the rest of the flow is unambiguous about which key is which. The recipient never needs the private key to *hold* the assets, only to run the one `claimlink` that moves them. + +## Step 2: announce the link + +```json +{ + "creator": "collector.wam", + "key": "PUB_K1_7fQQsgKNfRj3rKjf3RD2apBGN9HyEVsHCiHN3QgAzT64cWojRT", + "asset_ids": ["1099998460553"], + "memo": "birthday gift" +} +``` + +```ts +await session.transact({ + action: { + account: 'atomictoolsx', + name: 'announcelink', + authorization: [session.permissionLevel], + data: { + creator: session.actor, + key: linkPublicKey, + asset_ids: ['1099998460553'], + memo: 'birthday gift', + }, + }, +}) +``` + +- Required authorization: `creator`. +- RAM payer: `creator` pays for the new `links` row. +- Fails when: `asset_ids` is empty; the memo exceeds 256 characters; `creator` does not currently own one of the assets; a listed asset's template has `transferable: false`; or `creator` already has a live link for this exact set of asset ids ("You have already announced a link for these assets"). + +The assets stay in the creator's wallet at this point. The link is recorded with `assets_transferred: false`. + +Source: `src/link.cpp:21-98` + +## Step 3: fund the link + +Funding is a normal AtomicAssets `transfer` to the `atomictoolsx` account with the memo `"link"`; there is no atomictools action for it. The asset ids must be the same set announced in step 2 (order does not matter). + +```json +{ + "from": "collector.wam", + "to": "atomictoolsx", + "asset_ids": ["1099998460553"], + "memo": "link" +} +``` + +```ts +await session.transact({ + action: { + account: 'atomicassets', + name: 'transfer', + authorization: [session.permissionLevel], + data: { + from: session.actor, + to: 'atomictoolsx', + asset_ids: ['1099998460553'], + memo: 'link', + }, + }, +}) +``` + +The `atomictoolsx` contract catches the transfer via its `atomicassets::transfer` notification handler, matches it to the announced link by (sender, exact asset-id set) rather than by any link id, sets `assets_transferred: true`, and emits `loglinkstart`. + +- The memo must be exactly `"link"`. Any other memo, empty included, reverts the whole transfer with "Invalid memo", so the assets never leave the sender. +- If no announced link by this sender for this exact asset set exists, the transfer reverts with "No announced link by this sender for these assets exists". Announce first, then fund. +- Because funding is matched by asset set, a creator can hold only one live link per exact set of ids at a time (the duplicate check in step 2 enforces this). + +Source: `src/link.cpp:189-241` + +## Step 4: claim the link + +The recipient runs `claimlink` from whatever account they want to receive the assets. The proof is a signature, made with the link's private key, over the claimer's own account name. + +```json +{ + "link_id": 6572116, + "claimer": "newowner.wam", + "claimer_signature": "SIG_K1_KfPL..." +} +``` + +```ts +// signatureData is sha256 of the raw claimer account-name string +const digest = Checksum256.hash(Bytes.from('newowner.wam', 'utf8')) +const claimerSignature = linkPrivateKey.signDigest(digest) + +await session.transact({ + action: { + account: 'atomictoolsx', + name: 'claimlink', + authorization: [session.permissionLevel], + data: { + link_id: 6572116, + claimer: session.actor, + claimer_signature: claimerSignature, + }, + }, +}) +``` + +The contract computes `sha256(claimer)` over the raw account-name characters, recovers the public key from that digest and `claimer_signature`, and requires it to equal the link's stored `key`. On success it transfers the assets to `claimer` (memo `"Claimed link"`) and erases the link row. + +- Required authorization: `claimer` (which also routes the assets to that account). +- RAM: the claimer pays their own CPU/NET. If `claimer` has never held an AtomicAssets asset before, the `atomictoolsx` contract account pays the RAM for the recipient's new asset scope, because it is the `from` of the outgoing transfer. +- Fails when: the link was never funded ("The assets for this link have not yet been transferred to the atomic tools account"); or the signature does not recover the stored key ("The signature provided is not valid"). + +Why the claimer's name is the signed message: it binds the signature to one account. A signature made for `newowner.wam` recovers the link key only against the digest of `newowner.wam`; substitute a different claimer and the digest changes, the recovered key no longer matches, and the claim fails. Someone watching the mempool cannot copy a pending `claimlink` signature and reclaim the assets to their own account, because they would need to re-sign their own name with the private key they do not have. + +Source: `src/link.cpp:133-159` + +## Cancel a link + +The creator can reclaim the assets at any time before a successful claim. + +```json +{ "link_id": 6572116 } +``` + +```ts +await session.transact({ + action: { + account: 'atomictoolsx', + name: 'cancellink', + authorization: [session.permissionLevel], + data: { link_id: 6572116 }, + }, +}) +``` + +- Required authorization: the link's `creator`. +- If the link was funded, the assets are returned to `creator` via an inline AtomicAssets `transfer` with memo `"Cancelled link"`; if it was only announced and never funded, the row is simply erased. +- Fails when: no link with `link_id` exists, or the caller is not its creator. + +Source: `src/link.cpp:107-124` + +## RAM and authorization notes + +- The `links` row RAM is paid by the creator from `announcelink` onward and released when the link is claimed or cancelled. Funding the link (`assets_transferred` flip) keeps the same payer, so a claim does not shift RAM cost onto the recipient for the link row itself. +- The escrowed NFTs live in the `atomictoolsx` account's own AtomicAssets scope between funding and resolution. The contract moves them out under `permission_level{atomictoolsx, "active"}`, so the deployed account's `active` permission must be able to call `atomicassets::transfer` (it is the contract's own authority, so this holds by default). +- A first-time recipient's new asset scope RAM is paid by `atomictoolsx` on claim, since the contract is the transfer sender. +- The unrelated `auth` action always throws by design; it is a proof-of-key-control challenge for off-chain services, not part of the link flow. See `reference/atomictools/actions.md` ("Admin and off-chain auth"). + +## Reading links via chain tables + +Every live link is a row in the `links` table, scope `atomictoolsx`. Read the primary index by `link_id`: + +```bash +curl -s https://wax.greymass.com/v1/chain/get_table_rows -d '{ + "code": "atomictoolsx", "scope": "atomictoolsx", "table": "links", + "json": true, "limit": 2, "lower_bound": 3265012 +}' +``` + +```json +{"rows":[ + {"link_id":3265012,"creator":"yrgb4.wam","key":"EOS6YxGQNSuv5hk2YRjHRZN9rpvJAf323ZfRPzp22C8fy8pW8kfmB", + "asset_ids":["1099925607623"],"assets_transferred":1,"memo":""}, + {"link_id":3265020,"creator":"yrgb4.wam","key":"EOS86kigUFK22YzJspnLsHukJzszA2tNid8J4nCerBQyfYT2c2Y8K", + "asset_ids":["1099925607630"],"assets_transferred":1,"memo":""}], + "more":true,"next_key":"3265045"} +``` + +To find a link by the assets escrowed in it rather than by id, query the `assetidshash` secondary index (index position 2, `key_type` `sha256`). The key is the `sha256` of the sorted asset-id array, the same value the contract computes internally: + +```bash +curl -s https://wax.greymass.com/v1/chain/get_table_rows -d '{ + "code": "atomictoolsx", "scope": "atomictoolsx", "table": "links", + "json": true, "limit": 2, "index_position": 2, "key_type": "sha256" +}' +``` + +The `config` singleton reports the running version and the next link id to be assigned: + +```bash +curl -s https://wax.greymass.com/v1/chain/get_table_rows -d '{ + "code": "atomictoolsx", "scope": "atomictoolsx", "table": "config", "json": true, "limit": 1 +}' +``` + +```json +{"rows":[{"version":"1.0.0","link_counter":6572117,"atomicassets_account":"atomicassets"}],"more":false,"next_key":""} +``` + +The table only holds unresolved links: a claimed or cancelled link's row is erased. A row's `key` prints in the legacy `EOS...` format from `get_table_rows` and in the `PUB_K1_...` format from the hosted API; both encode the same key. The chain table carries no notion of "claimed" versus "cancelled", only `assets_transferred`; the lifecycle distinction is added by the indexer below. + +## Reading links via the hosted API + +The atomicassets-api exposes an `atomictools` namespace alongside `atomicassets` and `atomicmarket`. It resolves each link's assets to full AtomicAssets objects and tracks a lifecycle state the chain table does not keep, because it retains rows after a link resolves. + +```bash +curl -s "https://wax.api.atomicassets.io/atomictools/v1/links?limit=2" +``` + +Each element carries `tools_contract`, `link_id`, `assets_contract`, `creator`, `claimer` (null until claimed), `state`, `memo`, the resolved `assets` array, `public_key`, and the usual `created_at_*` / `updated_at_*` fields. A single link is available at `/atomictools/v1/links/{link_id}`, and `/atomictools/v1/config` returns `{atomictools_contract, atomicassets_contract, version}`. + +The `state` field is the indexer's `LinkState`, driven by the contract's log actions and terminal actions: + +| `state` | Name | Set when | +| --- | --- | --- | +| 0 | `WAITING` | `lognewlink` fired: announced, assets not yet transferred in | +| 1 | `CREATED` | `loglinkstart` fired: assets funded, link is claimable | +| 2 | `CANCELED` | `cancellink` ran | +| 3 | `CLAIMED` | `claimlink` ran; `claimer` is now set | + +Filter by state to list only claimable links (`?state=1`) or only claimed ones (`?state=3`); a live page of results returned states 1, 2, and 3 as expected, with `claimer` populated only on the `CLAIMED` rows. + +Source: `atomicassets-api src/filler/handlers/atomictools/index.ts:29-33` (the `LinkState` enum), `src/filler/handlers/atomictools/processors/links.ts:20-90` (the state transitions); live probes of `wax.api.atomicassets.io/atomictools/v1/links` and `/config`. + +## See also + +- `reference/atomictools/actions.md`: every action's parameters, auth, and notifications, including the signature-verification detail. +- `reference/atomictools/tables.md`: the `links` and `config` schemas and the `assetidshash` index. +- `guides/asset-lifecycle.md`: minting and transferring the assets that flow into a link. +- `reference/api.md`: hosted-API pagination limits and conventions shared across namespaces. diff --git a/guides/notification-integration.md b/guides/notification-integration.md new file mode 100644 index 0000000..bf85f08 --- /dev/null +++ b/guides/notification-integration.md @@ -0,0 +1,158 @@ +--- +scope: Building a third-party contract that reacts to atomicassets notifications - which notifications reach a listener, how to wire on_notify handlers, and the same-transaction safety rules +depends-on: [reference/atomicassets/notifications.md, reference/atomicassets/actions.md] +key-modules: ["atomicassets-contract (v2.0.0-rc4): src/atomicassets.cpp, include/atomicassets.hpp"] +--- + +# Building a reactive contract on AtomicAssets notifications + +A smart contract can react to AtomicAssets activity by receiving the `require_recipient` notifications the contract emits: a listener contract watches transfers, mints, or burns and runs its own logic the moment they happen, without polling the chain. This guide covers which notifications a third-party contract can receive, how to wire the C++ `on_notify` handlers so they actually fire, and the same-transaction safety rules that govern what a handler may safely do. It builds on `reference/atomicassets/notifications.md`, which is the reference for the collection-config side (`notify_accounts`, `allow_notify`) and the full per-action notification map; read that first, then this for the integration mechanics. Baseline is the V2 contract, tag `v2.0.0-rc4` of `atomicassets-contract` (the release pinned for both testnets). + +## Which notifications a listener can receive + +There are two independent ways a third-party contract gets notified, and they carry different data. + +### Directly, as a named party + +Some actions call `require_recipient` on the specific accounts involved, so a listener that is one of those accounts is notified even without any collection opt-in. `transfer` notifies both `from` and `to`: a contract set as the recipient of a transfer receives the `atomicassets::transfer` notification with the transfer's own parameters. `mintasset` notifies the `new_asset_owner` through its inline `logmint` action, so a contract that assets are minted directly to is notified as the owner. Offer creation notifies the offer's `sender` and `recipient` through `lognewoffer`. None of these require the listener to be on any collection list; being the party named in the action is enough. + +Source: `atomicassets-contract src/atomicassets.cpp:76-86` (`transfer`, `require_recipient(from)` and `require_recipient(to)` at `:83-84`), `atomicassets-contract src/atomicassets.cpp:1488-1505` (`logmint`, `require_recipient(new_asset_owner)` at `:1502`), `atomicassets-contract src/atomicassets.cpp:1457-1469` (`lognewoffer`) + +### Indirectly, as a collection notify account + +A collection author can add a contract to that collection's `notify_accounts` list (gated by `allow_notify`), and the contract is then notified on every collection-touching action through the inline `log*` action that fans out to `notify_collection_accounts`. This is how a listener observes activity for assets it does not own: mints, burns, transfers, data edits, and RAM-payer reassignments across the whole collection. The listener does not choose to subscribe; the collection author adds it, which is a deliberate trust grant, because a notify account's handler runs inside the triggering transaction and can make that action fail. The config side (adding and removing accounts, the one-way `forbidnotify` gate, the 24-account cap) is documented in `reference/atomicassets/notifications.md`; this guide assumes the account is already on the list and focuses on the receiving contract. + +Source: `atomicassets-contract src/atomicassets.cpp:1880-1888` (`notify_collection_accounts`), `atomicassets-contract src/atomicassets.cpp:246-279` (`addnotifyacc`, and its comment: "NOTE: It will consequently allow the account to make any of these actions throw (fail). Only add trusted accounts to this list") + +The full table of which action notifies whom, and by which mechanism, is in `reference/atomicassets/notifications.md` ("Which actions notify whom"). The listener sees no difference in wiring between the two paths: both arrive as an `on_notify` dispatch on some action name. What differs is the action name to bind and the data delivered. + +## Wiring the C++ handler + +A handler binds to a notification with the `on_notify` attribute and a parameter list that must match the notifying action's ABI exactly: + +```cpp +[[eosio::on_notify("atomicassets::transfer")]] +void on_transfer(name from, name to, std::vector asset_ids, std::string memo); +``` + +The string is `contract::action`. For the direct path, bind the primary action the party is named on: `atomicassets::transfer`, and `atomicassets::logmint` for mints (the notification is sent from the inline `logmint`, not from `mintasset` itself). For the collection path, bind the inline `log*` action that carries the fan-out: `atomicassets::logtransfer`, `atomicassets::logmint`, `atomicassets::logburnasset`, `atomicassets::logsetdata`, `atomicassets::logsetdatatl`, `atomicassets::logrampayer`. + +The trap: the handler's parameter list must match the notifying action's ABI parameters exactly, in order and type. If it does not, the handler is silently never dispatched. There is no error and no failed transaction; the notification simply does not match the handler, so the reaction just never runs. The exact signatures, from the pinned contract, are: + +| Bind to | Parameters, in order | +| --- | --- | +| `atomicassets::transfer` | `name from, name to, vector asset_ids, string memo` | +| `atomicassets::logtransfer` | `name collection_name, name from, name to, vector asset_ids, string memo` | +| `atomicassets::logmint` | `uint64_t asset_id, name authorized_minter, name collection_name, name schema_name, int32_t template_id, name new_asset_owner, ATTRIBUTE_MAP immutable_data, ATTRIBUTE_MAP mutable_data, vector backed_tokens, ATTRIBUTE_MAP immutable_template_data` | +| `atomicassets::logburnasset` | `name asset_owner, uint64_t asset_id, name collection_name, name schema_name, int32_t template_id, vector backed_tokens, ATTRIBUTE_MAP old_immutable_data, ATTRIBUTE_MAP old_mutable_data, name asset_ram_payer` | +| `atomicassets::logsetdata` | `name asset_owner, uint64_t asset_id, ATTRIBUTE_MAP old_data, ATTRIBUTE_MAP new_data` | +| `atomicassets::logrampayer` | `name asset_owner, uint64_t asset_id, name old_ram_payer, name new_ram_payer` | + +`ATTRIBUTE_MAP` and the `asset`/`name` types are the AtomicAssets serialization types; a listener that only needs the ids and account names still has to declare the trailing map and vector parameters so the whole signature matches, even if it ignores them. `logtransfer` is per collection: a multi-collection transfer sends one `logtransfer` per collection, so a collection notify account is called once per collection it is subscribed to, not once for the whole transfer. + +The data a handler receives is exactly the notifying action's parameters, nothing more. `logtransfer` carries the moved `asset_ids` but not their templates or backed tokens; `logburnasset` is the richest, carrying the burned asset's full deserialized immutable and mutable data plus its backed tokens, because after the burn the row is gone and this is the only place that data survives. A handler that needs anything beyond what its signature delivers must read the AtomicAssets tables itself within the same transaction. + +Source: `atomicassets-contract include/atomicassets.hpp:30-35` (`transfer`), `atomicassets-contract include/atomicassets.hpp:247-253` (`logtransfer`), `atomicassets-contract include/atomicassets.hpp:275-286` (`logmint`), `atomicassets-contract include/atomicassets.hpp:316-326` (`logburnasset`), `atomicassets-contract include/atomicassets.hpp:288-293` (`logsetdata`), `atomicassets-contract include/atomicassets.hpp:303-308` (`logrampayer`), `atomicassets-contract src/atomicassets.cpp:1752-1760` (per-collection `logtransfer` fan-out) + +## Semantics and safety + +### Same-transaction atomicity + +Your handler's abort is the user's abort. A notification is delivered inline, in the same transaction as the action that triggered it. The handler is not a callback on a queue; it runs as part of the triggering transaction, and if it throws, the whole transaction reverts, including the user's transfer or mint. This is stated directly in the contract's own warning on `addnotifyacc`: adding a notify account "will consequently allow the account to make any of these actions throw (fail)." The practical rule is to keep handlers cheap and non-throwing on paths you do not intend to veto. A `check()` that fails inside a transfer handler blocks a transfer the user expected to succeed, and from the user's side it looks like AtomicAssets itself rejected the transfer. Only assert when blocking the action is the deliberate intent of the integration, not as incidental input validation. + +Source: `atomicassets-contract src/atomicassets.cpp:246-250` (`addnotifyacc` warning comment), `reference/atomicassets/notifications.md` ("What a notified contract can and cannot rely on") + +### Authenticate the notifier, not the payload's account names + +The robust way to know a notification is genuine is that the dispatch itself binds it: an `on_notify("atomicassets::transfer")` handler only runs because the AtomicAssets contract sent the notification, so the notifier's identity is established by the binding, not by anything in the parameters. AtomicAssets models this in its own token-deposit handler: when it receives a `*::transfer` notification it authenticates the sending token contract with `get_first_receiver()` rather than trusting the transfer's fields, and it guards `if (to != get_self()) return;` so it only acts when it is the actual recipient. A listener bound to a single `contract::action` already knows the notifier, but the role guard matters: `transfer` notifies both `from` and `to`, so a handler is invoked in both roles and must check which one it is (`to == get_self()` for incoming) before acting. Do not treat the account names in the payload as authenticated identities to make trust decisions about; they are data describing the action, not proof that any of those accounts authorized your handler. + +Source: `atomicassets-contract src/atomicassets.cpp:1402-1416` (`receive_token_transfer`: `to != get_self()` guard at `:1403`, `get_first_receiver()` authentication at `:1412`) + +### Authorization context + +Assume none of the notified parties authorized your handler. The precise authorizations visible inside an Antelope notification handler are a property of the chain runtime (nodeos), not of the pinned AtomicAssets source, so this guide does not assert a line-cited rule about what `require_auth` returns in that context. The safe practice, and the one AtomicAssets follows in its own code, is to not attempt to authenticate a notified party by calling `require_auth` on a name from the payload: a handler establishes trust from the notifier binding (above), not by re-checking the user's authority. Where the contract needs its own authority, it uses `get_self()` (as every `log*` action does with `require_auth(get_self())`), never the notified user's. Design the handler to need only its own authorization plus the delivered data. + +Source: `atomicassets-contract src/atomicassets.cpp:1452` (`logtransfer` uses `require_auth(get_self())`), `atomicassets-contract src/atomicassets.cpp:1500` (`logmint` likewise); Antelope runtime authorization semantics are not covered by the pinned contract source and are stated here as safe practice, not a source-cited fact. + +### RAM + +Your handler bills its own account for the rows it stores. A row a handler emplaces into its own tables is paid for by whoever the handler names as payer. The general Antelope rule about which payer a notification context may bill is a runtime property, not something the pinned source states, so treat it conservatively: pay RAM from `get_self()` for anything the listener stores, rather than trying to bill a user who merely appears in the notification. AtomicAssets' own deposit handler never opens a new user-billed row from the notification path; it modifies an already-existing balance row with `same_payer` and requires the row to have been created earlier by a separate user-authorized `announcedepo`. A listener that accumulates state should budget for that RAM on its own account, the same way the worked example below emplaces under `get_self()`. + +Source: `atomicassets-contract src/atomicassets.cpp:1435-1437` (`receive_token_transfer` modifies with `same_payer`), `atomicassets-contract src/atomicassets.cpp:1418-1421` (deposit requires the pre-existing row); the general RAM-payer rule for notification contexts is Antelope runtime behavior, stated here as safe practice. + +### Inline follow-up actions stay in the transaction + +Deferring work to a self-inline action does not decouple it from the transaction. A handler can organize its follow-up work by sending an inline action to itself (`action(permission_level{get_self(), "active"}, ...).send()`), which is exactly how AtomicAssets dispatches its own `log*` actions. This gives the follow-up a clean authorization context (it runs as `get_self()`) and keeps the handler body small, but it does not make the work asynchronous: an inline action executes in the same transaction and still aborts the user's action if it throws. There is no built-in escape from the same-transaction rule in the notification path. Use a self-inline action for code structure and authorization clarity, not as a way to make a risky handler safe to fail. + +Source: `atomicassets-contract src/atomicassets.cpp:1752-1760` (AtomicAssets sends its own `logtransfer` as an inline action under `get_self()`), `atomicassets-contract src/atomicassets.cpp:1655-1663` (`RAM_RESTRICTIONS` note on same-transaction inline behavior) + +## A minimal listener: counting incoming transfers + +This skeleton reacts to assets transferred into its own account and keeps a per-sender count in a `stats` table. It is illustrative, not deployed; it is written to be consistent with the pinned `atomicassets::transfer` ABI (`asset_ids` as `vector`, `memo` as `string`). It shows the three load-bearing pieces: the exact-match signature, the role guard, and storing under `get_self()`. + +```cpp +#include +using namespace eosio; + +CONTRACT collector : public contract { +public: + using contract::contract; + + // Fires for both the `from` and `to` of every atomicassets transfer that + // names this account. The parameter list matches atomicassets::transfer + // exactly; if it did not, this handler would silently never run. + [[eosio::on_notify("atomicassets::transfer")]] + void ontransfer(name from, name to, std::vector asset_ids, std::string memo) { + if (to != get_self()) { + return; // we are the `from` side of someone else's transfer; ignore + } + + stats_t stats(get_self(), get_self().value); + auto itr = stats.find(from.value); + if (itr == stats.end()) { + stats.emplace(get_self(), [&](auto& row) { // this contract pays the RAM + row.sender = from; + row.received = asset_ids.size(); + }); + } else { + stats.modify(itr, same_payer, [&](auto& row) { + row.received += asset_ids.size(); + }); + } + } + +private: + TABLE stat_s { + name sender; + uint64_t received; + uint64_t primary_key() const { return sender.value; } + }; + typedef multi_index stats_t; +}; +``` + +The handler never throws, so it can never block an incoming transfer; it bills its own account for new rows; and it acts only when it is the recipient. A listener that instead watches a whole collection would bind `atomicassets::logtransfer` (adding the leading `name collection_name` parameter) and would need to be on that collection's `notify_accounts` list. + +## Testing the handler + +Handlers like this can be exercised in-process, without deploying to a chain, using the VeRT testing library: it runs the AtomicAssets contract and the listener together in a simulated environment, so a test can call `transfer` and assert on the listener's `stats` table, including the same-transaction abort behavior (a throwing handler failing the transfer). See `guides/testing-with-vert.md` for the setup. + +## Correct and avoid + +Correct: + +- Match the notifying action's ABI signature exactly, in order and type; declare even the parameters you ignore. A mismatched signature means the handler silently never fires. +- Guard on your role before acting: `transfer` notifies both `from` and `to`, so check `to == get_self()` (or `from`, as needed) first. +- Establish trust from the `on_notify` binding, and pay RAM for stored rows from `get_self()`. +- Keep handlers cheap and non-throwing except where blocking the action is the deliberate purpose of the integration. +- Read the AtomicAssets tables within the handler if you need data beyond the notification's parameters. + +Avoid: + +- Assuming a `notify_accounts` entry is a passive, read-only feed. It is not: your handler runs in the triggering transaction and your abort reverts the user's action, which is why the contract calls adding a notify account a trust decision (`reference/atomicassets/notifications.md`; `reference/atomicassets/structure.md`, "a notify account with a throwing `on_notify` handler can block the action"). +- Expecting notifications for actions that send none. `canceloffer` and `declineoffer` notify nobody, and in V2 `logbackasset` is a dead stub, so no backing notification arrives (`reference/atomicassets/notifications.md`). +- Relying on `asset_ids` grouping order across a multi-collection transfer: the per-collection `logtransfer` calls follow `std::map` key order, not the caller's asset-id order (`reference/atomicassets/notifications.md`). +- Doing settlement-shaped work inside a handler, such as an `on_notify` handler that creates a second AtomicAssets offer ahead of a market fulfillment. That is the buyoffer anti-pattern in `guides/buyoffers.md`; the same-transaction coupling makes it fragile. +- Treating a self-inline action as a way to make a failure-prone handler safe: the inline action still runs in the same transaction and still aborts the user's action on a throw. + +Source: `atomicassets-contract src/atomicassets.cpp:246-250` (trust warning), `reference/atomicassets/notifications.md`, `reference/atomicassets/structure.md` ("Authorization and the 24-account cap"), `guides/buyoffers.md` diff --git a/guides/testing-with-vert.md b/guides/testing-with-vert.md new file mode 100644 index 0000000..249e0d7 --- /dev/null +++ b/guides/testing-with-vert.md @@ -0,0 +1,220 @@ +--- +scope: Testing Antelope contracts in-process with @atomichub/vert, including AtomicAssets/AtomicMarket integrations +depends-on: [reference/atomicassets/actions.md] +key-modules: ["@atomichub/vert 2.2.0 (commit a8a4160): src/antelope/blockchain.ts, src/antelope/vm.ts, src/antelope/table.ts, examples/"] +--- + +# Testing Antelope contracts with VeRT + +VeRT runs a compiled Antelope contract's WebAssembly in-process under any JavaScript test runner, with no `nodeos`, no Docker, and no chain connection. It emulates the parts of the Antelope state machine a contract touches at runtime: action dispatch, the multi-index table store with secondary indexes, inline actions, notifications, permissions, and the crypto and print host functions. A test loads a `.wasm` and its `.abi`, calls actions the same way a transaction would, and reads the resulting tables, all synchronously in one process. + +This suits fast, deterministic unit and integration tests of contract logic: fee math, guard conditions, table state transitions, and the notification and inline-action wiring between contracts such as AtomicAssets and AtomicMarket. It does not replace testnet validation. VeRT does not model CPU/NET/RAM billing, real signature verification, block production, or deferred transactions, and its permission checks are a simplified subset (see "Authorization model"). Run the suite for logic coverage on every change; validate resource use, ordering, and end-to-end behavior against a real chain (jungle4 or wax-testnet) before deploying. + +`@atomichub/vert` is a fork of `@waxio/vert` that adds a per-chain host-function gate so a suite links only the host functions the target chain provides. Line citations below are against `@atomichub/vert` 2.2.0 at commit `a8a4160`. + +Source: `@atomichub/vert package.json` (name, version 2.2.0, description), `@atomichub/vert README.md` + +## What a test looks like + +Install the library as a dev dependency: + +```shell +npm install --save-dev @atomichub/vert +``` + +A test instantiates a `Blockchain`, loads the contract onto an account, calls an action, and asserts on a table row. This is the `eosio.token` suite that ships with the library, reading the `stat` singleton after `create`: + +```ts +import fs from "fs"; +import { Asset, Name } from "@wharfkit/antelope"; +import { Blockchain, nameToBigInt, symbolCodeToBigInt, antelopeAssert } from "@atomichub/vert"; +import { expect } from "chai"; + +const blockchain = new Blockchain(); +const eosioToken = blockchain.createAccount({ + name: Name.from("eosio.token"), + wasm: fs.readFileSync("contracts/eosio.token/eosio.token.wasm"), + abi: fs.readFileSync("contracts/eosio.token/eosio.token.abi", "utf8"), +}); +blockchain.createAccount("alice"); + +beforeEach(() => { + blockchain.resetTables(); +}); + +it("create", async () => { + await eosioToken.actions.create(["alice", "1000.000 TKN"]).send(); + const symcode = symbolCodeToBigInt(Asset.SymbolCode.from("TKN")); + expect(eosioToken.tables.stat(symcode).getTableRow(symcode)).to.deep.equal({ + supply: "0.000 TKN", + max_supply: "1000.000 TKN", + issuer: "alice", + }); +}); +``` + +Action arguments are passed positionally in an array matching the ABI field order, then `.send(authorization)`. With no argument, `.send()` authorizes as the contract account's own `active` permission; a bare string like `.send("alice")` is expanded to `alice@active`. A failed action throws, and the thrown message for a contract `check(false, ...)` is `eosio_assert: `, which the exported `antelopeAssert` helper builds for you. + +Source: `@atomichub/vert src/antelope/tests/blockchain.spec.ts` (executed: `eos-vm > eosio.token`), `@atomichub/vert src/antelope/account.ts:124-153` (`.send()` authorization defaulting), `@atomichub/vert src/antelope/errors.ts:1` (`antelopeAssert`) + +### Loading a contract, and when its actions are ready + +`createAccount` with `wasm`/`abi` read synchronously through `fs.readFileSync` wires the account's `actions` and `tables` in the constructor, so they are callable immediately. `blockchain.createContract(name, folder)` is the shorthand the README shows: it reads `folder.wasm` and `folder.abi` (both must share the base name) and enables inline actions by default. Because `createContract` reads those files as promises, the account's `actions` and `tables` are wired only after that load resolves on a later tick, not in the same tick as the call: + +```ts +const c = blockchain.createContract("mycontract", "build/mycontract"); +typeof c.actions.myaction; // "undefined" in the same tick +await new Promise((r) => setTimeout(r, 0)); +typeof c.actions.myaction; // "function" +``` + +Inside an `async` test body you are already past that tick, so `createContract` at the top of a spec and actions called inside `it(...)` blocks work as written. If you need the actions synchronously in the same tick, construct with `createAccount` and `fs.readFileSync` buffers instead. + +Every `.send()` and `.read()` re-instantiates each account's VM and awaits it before dispatch, so an action always runs against a freshly built VM and you do not normally call `recreateVm()` yourself. Call `await account.recreateVm()` only when you need a VM instance ready outside a transaction, such as poking `account.vm.imports.env.*` directly; it re-instantiates an already-loaded contract's WebAssembly and does not perform the async `.wasm`/`.abi` load. + +Source: `@atomichub/vert src/antelope/blockchain.ts:264-272` (`createContract`), `@atomichub/vert src/antelope/account.ts:55-91` (async `setContract`, `recreateVm`), `@atomichub/vert src/antelope/blockchain.ts:300-312` (`resetVm` recreates and awaits every account's VM per transaction). Load-timing behavior confirmed by executing both patterns against the bundled `foo` contract. + +## The per-chain host-function gate + +Antelope chains do not all expose the same host functions. A contract that imports a host function the target chain lacks is rejected by that chain at `setcode`, so a harness that offered it anyway would pass a suite the chain would never run. VeRT withholds chain-specific host functions unless you name the chain that provides them. + +`new Blockchain()` emulates generic Antelope and exposes no chain-specific host functions. `new Blockchain({ chain: "wax" })` adds the ones unique to WAX: + +```ts +const generic = new Blockchain(); // no verify_rsa_sha256_sig +const wax = new Blockchain({ chain: "wax" }); // adds verify_rsa_sha256_sig +``` + +The only WAX-specific function today is `verify_rsa_sha256_sig`, which WAX provides and EOS, Jungle4, and Vaulta do not. Under a generic (or unrecognized) chain, `verify_rsa_sha256_sig` is deleted from the import object, so a contract importing it fails to instantiate exactly as `setcode` would reject it off WAX. Test a WAX contract that uses RSA against `{ chain: "wax" }`, and test everything else against the default. Shared host functions such as `recover_key` survive the gate on every chain. + +The map of chain to its extra host functions is `CHAIN_SPECIFIC_HOST_FUNCTIONS`, exported from the library; add an entry there to model a new chain-specific function. + +Source: `@atomichub/vert src/antelope/blockchain.ts:21-29` (`CHAIN_SPECIFIC_HOST_FUNCTIONS`, `ALL_CHAIN_SPECIFIC_HOST_FUNCTIONS`), `@atomichub/vert src/antelope/blockchain.ts:75-77` (`enabledChainHostFunctions`), `@atomichub/vert src/antelope/vm.ts:1477-1485` (withholding by `delete imports.env[fn]`), `@atomichub/vert src/antelope/tests/vm.spec.ts:211-231` (executed: `chain-specific host functions`) + +## Time control + +The blockchain clock starts at epoch 0 (`TimePoint.fromMilliseconds(0)`), not wall-clock now, so `current_time_point()` reads 0 in a fresh harness. `setTime` moves the clock to an absolute point; `addTime` advances it by a duration (and `addBlocks` advances by 500ms per block). Use these to test time-gated logic such as auction expiry or listing windows without waiting. The bundled `timer` contract, whose action prints `current_time_point().time_since_epoch().count()`, reads 0, then 500000, then 1000000 microseconds as the clock is set forward: + +```ts +import { TimePoint } from "@wharfkit/antelope"; + +await time.actions.exec([timeName]).send(); +expect(time.bc.console).to.equal("0"); + +blockchain.setTime(TimePoint.fromMilliseconds(500)); +await time.actions.exec([timeName]).send(); +expect(time.bc.console).to.equal("500000"); +``` + +Anything a contract writes with `print` accumulates on `blockchain.console` (aliased as `contract.bc.console`) for the duration of one transaction and is cleared at the start of the next. + +Source: `@atomichub/vert src/antelope/blockchain.ts:67` (epoch-0 genesis), `@atomichub/vert src/antelope/blockchain.ts:277-295` (`setTime`, `addTime`, `subtractTime`, `addBlocks`), `@atomichub/vert examples/timer/timer.spec.ts` (executed end-to-end) + +## Authorization model + +VeRT's permission checks are a deliberate simplification of a real chain's, and in two respects they are stricter, so an action that a live chain would authorize can fail here. + +- **Single-name `require_auth` accepts only `active` or `owner`.** `require_auth(name)` is satisfied only if the authorization list carries `name` with permission exactly `active` or `owner`. There is no permission hierarchy and no linkauth resolution, so authorizing with a custom permission (`alice@mycustom`) does not satisfy `require_auth(alice)` even when a live chain would through a linked permission. `has_auth` follows the same rule. +- **`require_auth2` is an exact match.** `require_auth2(name, permission)` is satisfied only by that exact actor-and-permission pair in the authorization list. + +A permission named in an action's authorization must already exist on the account, or the action throws `Account has no permission ` before the WebAssembly runs. Accounts are created with `owner` and `active` only; to authorize with any other permission, add it first with `account.setPermissions(...)`. A `check(false, ...)` that fails on missing authority throws `missing required authority `. + +```ts +// alice has only owner and active, so this throws before the contract runs: +await token.actions.create(["alice", "1000 TKN"]).send("alice@custom"); +// Error: Account alice has no permission custom +``` + +Inline actions carry their own authorization and are checked against `eosio.code`: an inline action is permitted when the sending contract holds the `eosio.code` authority on the invoked permission, or the sender is a privileged account. Notifications, by contrast, run with an empty authorization list, so a notification handler cannot itself pass a `require_auth` for any account. + +Source: `@atomichub/vert src/antelope/vm.ts:131-147` (`require_auth`), `@atomichub/vert src/antelope/vm.ts:166-181` (`require_auth2`), `@atomichub/vert src/antelope/vm.ts:1697-1723` (permission-exists check and inline `eosio.code` check in `apply`), `@atomichub/vert src/antelope/account.ts:50,93-95` (default `owner`/`active`, `setPermissions`), `@atomichub/vert src/antelope/vm.ts:248-256` (notification context built with `authorization: []`). Permission-exists throw and default permissions confirmed by execution; the `missing required authority` message is exercised by `@atomichub/vert examples/foo/foo.spec.ts` and `src/antelope/tests/priv.spec.ts`. + +## Multi-contract setups: inline actions and notifications + +Load each contract onto its own account on the same `Blockchain` and they interact as on chain. `send_inline` dispatches an inline action to another loaded contract, and `require_recipient` delivers a notification to another contract that has a matching `[[eosio::on_notify]]` handler. Both are what an AtomicMarket sale relies on: `purchasesale` sends an inline `acceptoffer` to AtomicAssets, and AtomicAssets' `lognewoffer` notifies AtomicMarket (see `guides/offers.md`). VeRT runs that whole cascade in-process, appending each inline action and notification to the action-trace queue in execution order. + +The privileged-inline path is exercised by the bundled suite: a contract flagged `privileged: true` at creation can send an inline action without holding `eosio.code`, while a non-privileged sender is held to the `eosio.code` check. + +```ts +const privContract = blockchain.createAccount({ + name: Name.from("auth.require"), + wasm: fs.readFileSync("contracts/auth.require/auth.require.wasm"), + abi: fs.readFileSync("contracts/auth.require/auth.require.abi", "utf8"), + privileged: true, +}); +await privContract.actions.inlinetest(["user", "auth.require"]).send("notuser@active"); +``` + +An action that returns a value (or inlines an action that does) resolves `.send()` to an array of the deserialized return values in execution order; `.read()` returns a single value for read-only calls. + +For an AtomicAssets integration test the library ships a `createDummyNfts` helper that stands up a full collection through top-level actions, `init`, `admincoledit`, `createcol`, `createschema`, `createtempl`, then `mintasset` per account, so your test starts from real minted assets rather than injected rows: + +```ts +export const createDummyNfts = async ( + atomicassetsContract: Account, author: Account, mintToEach: number, accountsToMintTo: Account[] +) => { /* ... */ } +``` + +Source: `@atomichub/vert src/antelope/vm.ts:261-293` (`send_inline`), `@atomichub/vert src/antelope/vm.ts:215-259` (`require_recipient`), `@atomichub/vert src/antelope/blockchain.ts:79-130` (action-trace queue over inline actions and notifications), `@atomichub/vert src/antelope/tests/priv.spec.ts` (executed: privileged inline), `@atomichub/vert src/antelope/tests/return-values.spec.ts` (executed: `.send()` return arrays and `.read()`), `@atomichub/vert src/helpers/createDummyNfts.ts:10-68` + +## Known limits, and how to design around them + +These are not defects to work around blindly; they are boundaries of the emulation. Design tests so the contract itself drives state, and they stop mattering. + +### Transaction-context host functions do not work in notification or inline subcontexts + +`transaction_size` and `read_transaction` (and anything built on them) serialize the current context's transaction. VeRT populates that transaction only on the top-level action context; the contexts it builds for notifications and inline actions carry no transaction, so calling either host function inside a notification handler or an inline action throws rather than returning a size. At the top level they work: a top-level action reading `transaction_size()` returns a real byte count. + +```ts +await txn.actions.top([]).send(); // prints a real size, e.g. 50 +await txn.actions.ping(["receiver"]).send(); // receiver's on_notify calls transaction_size() -> throws +``` + +Design around it by asserting transaction-shape logic through a top-level action. If a contract's notification handler genuinely depends on `read_transaction`, that path needs testnet coverage instead. + +Source: `@atomichub/vert src/antelope/vm.ts:1210-1226` (`read_transaction`, `transaction_size` serialize `context.transaction`), `@atomichub/vert src/antelope/vm.ts:248-256` and `:282-290` (notification and inline contexts built without a `transaction`). Confirmed by execution: a top-level read returned 50; the same call in an `on_notify` handler threw. + +### setRow writes only the primary row; secondary indexes exist only for rows the contract emplaces + +`contract.tables.(scope).set(primaryKey, payer, data)` injects a primary row directly into the store. It writes the primary row only and does not populate any secondary index. So a table's secondary indexes hold entries only for rows the contract itself emplaced in-WASM (through `emplace`), not for rows you inject with `set`. Reading an injected row by primary key works; but a contract `modify` of a `set`-injected row whose table has a secondary index aborts, because the modify walks a secondary-index entry that was never created (the observed abort is a dereference of an end iterator). + +```ts +// Injected primary row reads back fine: +sec.tables.items(scope).set(pk, Name.from("sec"), { owner: "bob", value: 5 }); +sec.tables.items(scope).getTableRow(pk); // { owner: "bob", value: 5 } + +// But a contract modify of that injected row aborts on the missing secondary index: +await sec.actions.upd(["bob", 7]).send(); // throws +``` + +Design around it by driving rows through top-level actions wherever the test will later mutate them: emplace via the contract's own action (or a helper like `createDummyNfts`) so the secondary index is built, and reserve `set` for read-only fixtures and rows the test never modifies. Seeding read-only fixtures with `set` is exactly what the bundled `fixtures` example does. + +Source: `@atomichub/vert src/antelope/table.ts:454-474` (`TableView.set` writes the primary `KeyValueObject` only, no secondary-index write), `@atomichub/vert examples/fixtures/fixtures.spec.ts` (executed: `set` then `getTableRow`). The modify-abort was confirmed by executing a contract with a secondary index against a `set`-injected row. + +## Correct and avoid + +```text +// correct +new Blockchain() // generic Antelope; withholds chain-specific host fns +new Blockchain({ chain: "wax" }) // only when the contract imports verify_rsa_sha256_sig +emplace rows through top-level actions when the test will later modify them +seed read-only fixtures with tables.(scope).set(...) +assert transaction_size / read_transaction through a top-level action +reset state with blockchain.resetTables() in beforeEach +treat a thrown action as the failure signal; match on "eosio_assert: " + +// avoid +new Blockchain({ chain: "wax" }) for a non-WAX contract // masks a real setcode rejection +tables.(scope).set(...) for a row a contract action will later modify // modify aborts +calling transaction_size / read_transaction from a notification or inline handler // throws +authorizing with a custom permission and expecting require_auth(name) to pass // needs active/owner +calling actions in the same synchronous tick as createContract // actions wire on a later tick +treating a green VeRT run as deploy-ready // it does not model RAM/CPU/NET, signatures, or ordering +``` + +## See also + +- `guides/notification-integration.md`: the AtomicAssets notification wiring these multi-contract tests exercise. +- `guides/offers.md`: the inline-`acceptoffer` and `lognewoffer`-notification cascade a sale test drives through VeRT. +- `reference/atomicassets/actions.md` and `reference/atomicassets/notifications.md`: the action parameters and notify targets to assert against. +- `reference/wharfkit.md`: `@wharfkit/antelope` id serialization and typed table-read behavior, shared by the values you pass VeRT actions. diff --git a/reference/api-streaming.md b/reference/api-streaming.md new file mode 100644 index 0000000..9487d61 --- /dev/null +++ b/reference/api-streaming.md @@ -0,0 +1,95 @@ +--- +scope: atomicassets-api realtime surface - Socket.IO namespaces, events, room subscription, connection shape, and the absence of socket auth or app-layer limits +depends-on: [] +key-modules: + - "atomicassets-api (main, f6419858): src/api/server.ts, src/api/utils.ts, src/api/notification.ts, src/api/namespaces/*/routes/*.ts" +--- + +# atomicassets-api realtime (Socket.IO) + +## Transport, path, and connection URL shape + +The API attaches a Socket.IO server to the same HTTP server that serves the REST endpoints, so realtime and REST share one host and port. The server is constructed with `transports: ['websocket']`, `allowEIO3: true`, and `cors: {origin: '*'}`, so clients connect over the WebSocket transport only (HTTP long-polling is disabled) and both Engine.IO v3 and v4 clients are accepted from any origin. The Engine.IO path is the Socket.IO default `/socket.io`; realtime channels are Socket.IO namespaces, not URL routes. A client selects a channel by connecting to the namespace whose name matches the REST path plus the resource, for example `wss://wax.api.atomicassets.io/atomicmarket/v1/sales`. Use a Socket.IO v4 client (the server is `socket.io ^4.8.3`); pin the client to the server's major and force the WebSocket transport, because the default transport list starts with polling, which this server does not serve. + +Source: `atomicassets-api (main, f6419858) src/api/server.ts` (`SocketServer`, `new Server(..., {transports: ['websocket'], allowEIO3: true, cors: {origin: '*'}})`), `package.json` (`socket.io`), `src/api/utils.ts` (`createSocketApiNamespace` maps namespace name to `/v1/`); live probe of `wss://wax.api.atomicassets.io` connecting to five namespaces over the WebSocket transport (all handshakes succeeded) + +## Namespace and event catalog + +Each namespace name is the configured namespace path (`/atomicassets`, `/atomicmarket`, `/atomictools` on the reference deployment) followed by the resource path. Namespace paths are operator config (`namespaces[].path`), so a non-default deployment can serve them under other prefixes. Every event payload is a plain object carrying the triggering `transaction`, `block`, and `trace`, the affected entity id, and the fully formatted entity row (the same shape the matching REST endpoint returns), except `fork`, whose payload is only `{block_num}`. + +| Namespace | Event | Fired on (action trace) | Room | +| --- | --- | --- | --- | +| `/atomicassets/v1/assets` | `new_asset` | `logmint` | none (whole namespace) | +| `/atomicassets/v1/assets` | `burn` | `logburnasset` | none | +| `/atomicassets/v1/assets` | `back` | `logbackasset` | none, gated on `socket_features.asset_update` | +| `/atomicassets/v1/assets` | `update` | `logsetdata` | none, gated on `socket_features.asset_update` | +| `/atomicassets/v1/offers` | `create` | `lognewoffer` | none | +| `/atomicassets/v1/offers` | `new_transfer` | `logtransfer` | none | +| `/atomicmarket/v1/sales` | `new_sale` | `lognewsale` | `new_sales` | +| `/atomicmarket/v1/sales` | `purchased_sale` | `purchasesale` | `purchased_sales` | +| `/atomicmarket/v1/auctions` | `new_auction` | `lognewauct` | `new_auctions` | +| `/atomicmarket/v1/auctions` | `new_bid` | `auctionbid` | `new_bids` | +| `/atomicmarket/v1/buyoffers` | `new_buyoffer` | `lognewbuyo` | `new_buyoffers` | + +Every listed namespace also emits `fork` (payload `{block_num}`) to the whole namespace when the connected reader rolls back a microfork, so a consumer holding optimistic state can invalidate everything at or after that block. The `back` and `update` asset events are emitted only when the atomicassets namespace is configured with `socket_features.asset_update: true`; the reference config ships it `false`, so a default deployment broadcasts `new_asset`, `burn`, and `fork` on the assets namespace and nothing else. The `atomictools` namespace registers no socket handler, so it exposes no realtime events even though its REST namespace exists. + +Source: `atomicassets-api (main, f6419858) src/api/namespaces/atomicassets/routes/assets.ts` (`new_asset`/`burn`/`back`/`update`/`fork`, `asset_update` gate), `.../atomicassets/routes/offers.ts` (`create`), `.../atomicassets/routes/transfers.ts` (`new_transfer`), `.../atomicmarket/routes/sales.ts` (`new_sale`/`purchased_sale`), `.../atomicmarket/routes/auctions.ts` (`new_auction`/`new_bid`), `.../atomicmarket/routes/buyoffers.ts` (`new_buyoffer`), `.../atomicmarket/index.ts` (`socket()` wires sales/auctions/buyoffers), `.../atomictools/index.ts` (empty `socket()`), `config/server.config.example.json` (`socket_features.asset_update: false`) + +## What triggers a broadcast + +Socket events are not driven by the API's own database writes. The filler's notifier publishes each batch of matching action traces and table deltas to a Redis pub/sub channel named `eosio-contract-api:::api`; the API subscribes to that channel through `ApiNotificationReceiver`, and each socket route registered an `onData(, ...)` listener that queries the affected rows and emits the socket event. A deployment therefore broadcasts realtime events only when its API process shares Redis with a running filler whose reader is configured to publish notifications; an API pointed at a database with no live notifier serves REST normally but stays silent on every namespace. Connecting to a namespace succeeds regardless, because the handshake is independent of whether any notifier is publishing. + +Source: `atomicassets-api (main, f6419858) src/api/notification.ts` (`ApiNotificationReceiver`, channel `eosio-contract-api:::api`), `src/api/namespaces/*/index.ts` (`socket()` constructs the receiver from `args.connected_reader` and registers the per-resource `onData` listeners); live probe held five namespaces open for 30 and 50 second windows and observed no events, consistent with connectivity being independent of notifier traffic + +## Market namespaces gate events behind opt-in rooms; asset and offer namespaces do not + +The three atomicmarket namespaces broadcast their entity events only to Socket.IO rooms, and a fresh connection joins none of them. A client opts in by emitting a `subscribe` event whose payload sets each wanted room to a truthy value; the same handler leaves any room whose flag is absent or falsy, so `subscribe` is the full membership list on every call, not an additive toggle. The room names are `new_sales` and `purchased_sales` on `/atomicmarket/v1/sales`, `new_auctions` and `new_bids` on `/atomicmarket/v1/auctions`, and `new_buyoffers` on `/atomicmarket/v1/buyoffers`. Without a `subscribe` the market namespaces deliver only `fork`, which is broadcast namespace-wide. The `/atomicassets/v1/assets` and `/atomicassets/v1/offers` namespaces use no rooms: every subscriber receives every event the moment it connects, with no `subscribe` step. + +Source: `atomicassets-api (main, f6419858) src/api/namespaces/atomicmarket/routes/sales.ts`, `.../auctions.ts`, `.../buyoffers.ts` (each `namespace.on('connection', ...)` with a `subscribe` handler over a fixed `availableRooms` list; entity events use `namespace.in('').emit(...)` while `fork` uses `namespace.emit(...)`), `.../atomicassets/routes/assets.ts` and `.../offers.ts` (no connection handler, all events via `namespace.emit(...)`) + +## Transfers ride the offers namespace + +There is no `/v1/transfers` Socket.IO namespace. The transfers socket handler registers on `/v1/offers`, the same namespace the offers handler uses, so `logtransfer` broadcasts arrive as the `new_transfer` event on `/atomicassets/v1/offers` alongside the offer `create` event. A consumer that wants transfer events must connect to the offers namespace and listen for `new_transfer`; connecting to a `/v1/transfers` namespace name yields a valid but permanently silent connection. + +Source: `atomicassets-api (main, f6419858) src/api/namespaces/atomicassets/routes/transfers.ts` (`createSocketApiNamespace(this.server, this.core.path + '/v1/offers')`), `.../offers.ts` (same namespace path) + +## Template buyoffers emit no socket events at this commit + +`reference/api.md` records that template-buyoffer socket notifications fire only for new offers, never for cancellation or fulfillment. The source is consistent with and narrower than that: the `templateBuyofferSockets` handler emits `new_template_buyoffer` only on `lognewtbuyo` and has no cancel or fulfill branch, but at this commit that handler is defined and never called. The atomicmarket namespace's `socket()` wires only the sales, auction, and buyoffer handlers, so `/atomicmarket/v1/template_buyoffers` accepts connections and its `new_template_buyoffers` room exists in the dead code, yet no template-buyoffer event is broadcast. Track template-buyoffer state changes by polling `/atomicmarket/v1/template_buyoffers` rather than over the socket. Re-check this when the notifier wiring changes, since it is a one-line registration away from going live. + +Source: `atomicassets-api (main, f6419858) src/api/namespaces/atomicmarket/routes/template-buyoffers.ts` (`templateBuyofferSockets` defined, emits only on `lognewtbuyo`), `src/api/namespaces/atomicmarket/index.ts` (`socket()` calls `salesSockets`, `auctionSockets`, `buyofferSockets` only; no reference to `templateBuyofferSockets` anywhere in the tree) + +## Socket connections carry no app-layer auth or rate limit + +The Socket.IO server registers no connection or handshake middleware, so namespace connections are unauthenticated and uncapped by the application: any client may open any namespace and, on the market namespaces, `subscribe` to any room. The express-rate-limit middleware documented in `reference/api.md` is mounted on the REST namespace paths and never sees the WebSocket upgrade, which Socket.IO intercepts on the HTTP server ahead of express, so REST rate limiting does not bound socket connections or event volume. Any connection ceiling a consumer hits in practice comes from a fronting proxy or CDN, not the API. Treat the reference deployment's actual socket exposure as operator- and edge-specific, not a software guarantee. + +Source: `atomicassets-api (main, f6419858) src/api/server.ts` (`SocketServer` constructs `new Server(...)` with no `io.use(...)` and no per-namespace auth; the express `limiter` is mounted per REST namespace path in `src/api/namespaces/*/index.ts`, not on the Socket.IO server) + +## Consumption example + +The snippet below is the exact client used to validate the surface against the hosted WAX deployment. It connects to the busy assets namespace and to the sales namespace, opts into both sales rooms, logs every event, and disconnects. It requires a Socket.IO v4 client (`npm install socket.io-client@4`) and forces the WebSocket transport. + +```js +const { io } = require('socket.io-client'); + +const HOST = 'https://wax.api.atomicassets.io'; + +// Asset and offer namespaces need no subscribe: every event arrives on connect. +const assets = io(HOST + '/atomicassets/v1/assets', { transports: ['websocket'] }); +assets.on('connect', () => console.log('assets connected', assets.id)); +assets.on('new_asset', (e) => console.log('mint', e.asset_id, 'block', e.block.block_num)); +assets.on('burn', (e) => console.log('burn', e.asset_id)); +assets.on('fork', (e) => console.log('fork at', e.block_num)); + +// Market namespaces gate entity events behind rooms; subscribe on connect. +// Each subscribe call is the full room list, not an additive toggle. +const sales = io(HOST + '/atomicmarket/v1/sales', { transports: ['websocket'] }); +sales.on('connect', () => { + console.log('sales connected', sales.id); + sales.emit('subscribe', { new_sales: true, purchased_sales: true }); +}); +sales.on('new_sale', (e) => console.log('new sale', e.sale_id)); +sales.on('purchased_sale', (e) => console.log('purchased', e.sale_id)); +``` + +Source: live run against `wss://wax.api.atomicassets.io` with `socket.io-client 4.8.3`; every namespace handshake succeeded, confirming the namespace names and the WebSocket-only transport diff --git a/reference/api.md b/reference/api.md index 84c5d34..5a94379 100644 --- a/reference/api.md +++ b/reference/api.md @@ -21,7 +21,7 @@ The AtomicMarket API (eosio-contract-api) validates the `limit` query parameter ## Template buyoffers keep all lifecycle states -AtomicMarket template buyoffers in eosio-contract-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`. Socket notifications are emitted only for new offers; cancellation and fulfillment produce no broadcast. The filler and API state enums both encode LISTED=0, CANCELED=1, SOLD=2 and map 1:1. +AtomicMarket template buyoffers in eosio-contract-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. ## The `state` field means something different on each listing endpoint @@ -35,3 +35,13 @@ Every AtomicMarket listing endpoint returns a numeric `state`, but the enum diff Sales, auctions, and buyoffers share the value `0` for a not-yet-active listing (assets/funds not escrowed) and `2` for CANCELED, but the "settled" value is `3` (SOLD / ACCEPTED), not the `2` a template buyoffer uses. Do not carry a `SOLD=2` assumption from the template-buyoffer enum across to the other three endpoints. Source: `atomicassets-api src/api/namespaces/atomicmarket/index.ts` (`SaleApiState`, `AuctionApiState`, `BuyofferApiState`, `TemplateBuyofferApiState`) + +## Rate limits + +REST requests are rate limited per client IP when the deployment sets a `rate_limit` config block; with it unset there is no limiter. The limit is `rate_limit.requests` over a sliding window of `rate_limit.interval` seconds, and the counter lives in Redis (via `rate-limit-redis`) under a per-chain key prefix, so the budget is shared across every API replica for that chain rather than counted per process. The client is keyed by `req.ip`, resolved through the `trust_proxy` setting, so behind Cloudflare a correct CIDR `trust_proxy` list is what makes the bucket the real client rather than the edge. Over the limit the API returns HTTP 429 with `{"success": false, "message": "Rate limit"}`. IPs listed in `ip_whitelist` skip the limiter entirely (and the response cache with it). The limiter is mounted only on the REST namespace paths (`/atomicassets`, `/atomicmarket`, `/atomictools`), so the root operational routes (`/health`, `/alive`, `/healthc`, `/timestamp`, `/metrics`, `/docs`) are not rate limited, and neither are Socket.IO connections, which bypass express entirely (`reference/api-streaming.md`). + +Both header families are emitted: the standard draft `RateLimit-Limit`, `RateLimit-Policy`, `RateLimit-Remaining`, `RateLimit-Reset` and the legacy `X-RateLimit-*`. A `curl -sI` of `https://wax.api.atomicassets.io/atomicassets/v1/assets?limit=1` returns `ratelimit-limit: 240` and `ratelimit-policy: 240;w=60`, so the reference WAX deployment allows 240 requests per 60 seconds. Those numbers are deployment config, not a property of the software: the defaults live in the operator's `server.config.json`, other chains and mirrors set their own, and a value read from headers today can change on any redeploy. Bound client request rate to the `RateLimit-Remaining`/`RateLimit-Reset` the server actually returns rather than to a hardcoded ceiling. + +When `rate_limit.bill_execution_time` is enabled (it is `true` in the reference config), a request that takes longer than a second costs more than one hit: after the response is sent the limiter adds `ceil(elapsed_seconds) - 1` extra increments to the caller's bucket, so a slow multi-second query draws down the window faster than a fast one. A client that paginates with heavy filters can therefore hit 429 well before it has made `requests` calls. Budget for the billed cost of slow queries, not just the raw call count. + +Source: `atomicassets-api src/api/server.ts` (`WebServer` constructor: `rateLimit` with `RedisStore`, `ipKeyGenerator(req.ip)`, `ip_whitelist` skip, 429 `Rate limit` handler, `legacyHeaders`/`standardHeaders` both true, `bill_execution_time` post-send increment loop), `src/api/namespaces/*/index.ts` (`server.web.express.use(this.path, server.web.limiter)` mounts the limiter per namespace path), `src/types/config.ts` (`rate_limit.interval`/`requests`/`bill_execution_time`, `ip_whitelist`), `config/server.config.example.json` (`interval: 60`, `requests: 240`, `bill_execution_time: true`); live `curl -sI` of `https://wax.api.atomicassets.io/atomicassets/v1/assets?limit=1` returning `ratelimit-limit: 240`, `ratelimit-policy: 240;w=60`, and the `x-ratelimit-*` legacy set diff --git a/reference/atomicassets/tables.md b/reference/atomicassets/tables.md index c10a0ad..c81823c 100644 --- a/reference/atomicassets/tables.md +++ b/reference/atomicassets/tables.md @@ -109,7 +109,7 @@ Secondary indexes: none. Written only by `setschematyp`, which fully replaces the row's `format_type` vector on every call (not additive). Never passes through `atomicdata::serialize`/`deserialize`; these are plain ABI-serialized rows, not custom-binary blobs. Changed in V2: this table does not exist in V1. -Do not treat the API's `mediatype`/`info` fields as evidence of a `schematypes` row. The atomicassets-api includes `mediatype` and `info` on every attribute of a schema's `format` in its HTTP response even when the on-chain `schematypes` table has no row for that schema: with no descriptor set it defaults `mediatype` to the attribute's own `name` and `info` to `null`. To know whether a real `FORMAT_TYPE` descriptor was set, read the `schematypes` table on chain rather than inferring it from the API's schema response. +Do not treat the API's `mediatype`/`info` fields as evidence of a `schematypes` row. The atomicassets-api includes `mediatype` and `info` on every attribute of a schema's `format` in its HTTP response even when the on-chain `schematypes` table has no row for that schema: with no descriptor set it defaults `mediatype` to the attribute's own `name` and `info` to `null`. To know whether a real `FORMAT_TYPE` descriptor was set, read the `schematypes` table on chain rather than inferring it from the API's schema response. This synthesis ships in atomicassets-api 2.0.0 (live-confirmed on the wax-testnet deployment, which reports that version on `/health`); deployments on the 1.7 line, including the WAX mainnet reference deployment at the time of the read, return schema `format` entries as plain `{name, type}` with no `mediatype`/`info` fields at all. Source: `include/atomicassets.hpp:373-379`, `src/atomicassets.cpp:514-565` (`setschematyp`) diff --git a/reference/atomictools/actions.md b/reference/atomictools/actions.md new file mode 100644 index 0000000..68144eb --- /dev/null +++ b/reference/atomictools/actions.md @@ -0,0 +1,144 @@ +--- +scope: Complete action reference for the atomictools contract (links / claimlinks) +depends-on: [reference/atomictools/tables.md, reference/atomicassets/actions.md] +key-modules: ["atomictools-contract (commit d89ce79e4): src/link.cpp, src/auth.cpp, include/atomictoolsx.hpp"] +--- + +# AtomicTools actions + +Complete action reference for the `atomictools` contract, deployed as account `atomictoolsx` on WAX (and under the same account name on other Antelope chains). The contract has no release tags; every citation below pins commit `d89ce79e4` of `pinknetworkx/atomictools-contract`. Action declarations are in `include/atomictoolsx.hpp`; bodies are in `src/link.cpp` and `src/auth.cpp`. + +The contract's single purpose is transferable claim links: a creator escrows a set of AtomicAssets NFTs against an off-chain key pair, and anyone who holds the matching private key can later claim those assets to their own account. It moves no fungible tokens and holds no balances of its own; the only assets it ever custodies are the escrowed NFTs, held in the `atomictoolsx` account's own AtomicAssets scope between announcement and claim/cancel. + +Live-chain status: the full action and table list on this page was diffed against the deployed `atomictoolsx` ABI on WAX mainnet (`get_abi`) and matches the pinned source exactly. `config.version` reads `1.0.0` live. + +## Link lifecycle actions + +A link moves through announce (`announcelink`), fund (an AtomicAssets `transfer` with memo `"link"`, caught by the notification handler), then exactly one terminal step: `claimlink` (assets go to the claimer) or `cancellink` (assets go back to the creator). The row is erased on either terminal step. See `guides/links.md` for the end-to-end integrator flow. + +### announcelink + +- `creator: name` +- `key: public_key`: the link's public key. The matching private key is what gets shared out of band to whoever may claim. +- `asset_ids: vector`: must contain at least one id; every id must currently belong to `creator`. +- `memo: string`: up to 256 characters. + +Required authorization: `creator`. + +Creates a `links` row with `assets_transferred = false`, keyed by a contract-wide `config.link_counter` (starts at 1). Each listed asset must currently belong to `creator`, and any templated asset's template must have `transferable = true` (a non-transferable asset cannot be linked). RAM for the new row is paid by `creator` (`links.emplace(creator, ...)`). A creator may not have two live links for the exact same set of asset ids: the check compares the new id set against existing links sharing the same `asset_ids` hash and rejects a duplicate by the same creator with "You have already announced a link for these assets." Different creators may each hold a link over the same id set. Sends `lognewlink`, which notifies `creator`. + +This action only records the intent; it does not move the assets. Funding is a separate AtomicAssets `transfer` step (below). + +Source: `include/atomictoolsx.hpp:39-44`, `src/link.cpp:21-98` + +### cancellink + +- `link_id: uint64_t` + +Required authorization: the link's `creator`. + +Erases the `links` row. If the assets were already transferred in (`assets_transferred == true`), they are first returned to `creator` via an inline AtomicAssets `transfer` with memo `"Cancelled link"`; if they were never funded, the row is simply removed. Callable at any point before the link is claimed. Sends no `atomictools` log action of its own (the asset return surfaces only as the inline AtomicAssets `transfer` and its `logtransfer`). + +Source: `include/atomictoolsx.hpp:46-48`, `src/link.cpp:107-124` + +### claimlink + +- `link_id: uint64_t` +- `claimer: name`: the account the assets are claimed to. +- `claimer_signature: signature`: a signature over `sha256(claimer)` produced with the link's private key (see "How the claim proves knowledge of the link key" below). + +Required authorization: `claimer`. + +Transfers the link's assets to `claimer` via an inline AtomicAssets `transfer` with memo `"Claimed link"`, then erases the `links` row. Fails with "The assets for this link have not yet been transferred to the atomic tools account" if `assets_transferred` is false, and with "The signature provided is not valid" if the recovered key does not match the link's stored `key`. There is no separate log action; the claim is observable as the inline `transfer` (and the indexer keys off the top-level `claimlink` trace). + +Source: `include/atomictoolsx.hpp:50-54`, `src/link.cpp:133-159` + +## How the claim proves knowledge of the link key + +The claim is a challenge-response over the claimer's own account name, not over the assets or the link id. `claimlink` computes `claimer_digest = sha256(claimer.to_string())` (the raw account-name characters, not a serialized struct) and calls `recover_key(claimer_digest, claimer_signature)`, then requires the recovered public key to equal the link's stored `key`. Only a holder of the link's private key can produce a signature that recovers to that public key, so a valid `claimlink` proves the caller holds the private key that was shared for this link. + +Because the signed message is the claimer's own account name, a signature built for one claimer cannot be replayed by a different account: a different `claimer` hashes to a different digest, so the same signature recovers a different (wrong) key. This binds a claim to its account and defeats mempool front-running of a submitted `claimlink` (an observer who copies the signature but substitutes their own account name gets a digest the signature no longer matches, and they cannot re-sign without the private key). The claimer also signs the transaction itself under `require_auth(claimer)`, which is what routes the assets to their account. + +Source: `src/link.cpp:146-156` + +## Asset funding: the transfer notification handler + +### receive_asset_transfer + +- `from: name` +- `to: name` +- `asset_ids: vector` +- `memo: string` + +Not a directly callable action and not present in the contract ABI's action list: declared `[[eosio::on_notify("atomicassets::transfer")]]`, so it runs only as a notification handler when the `atomicassets` contract fans out a `transfer` receipt to `atomictoolsx`. + +Returns immediately if `to != get_self()`. Otherwise the memo must be exactly `"link"` (any other memo, including empty, hits `check(false, "Invalid memo")` and reverts the whole incoming transfer). On a `"link"` memo it locates the matching announced link by hashing the transferred `asset_ids` and walking the `assetidshash` secondary index for a row whose asset set is a permutation of the transfer's and whose `creator == from`; if none exists it reverts with "No announced link by this sender for these assets exists." The link is not referenced by `link_id` here: funding is matched by (sender, exact asset-id set), which is why a creator may hold only one live link per id set. On a match it sets `assets_transferred = true` (with `same_payer`, so the creator keeps paying the row RAM) and sends `loglinkstart`. + +Source: `include/atomictoolsx.hpp:57-62`, `src/link.cpp:189-241` + +## Notification / log actions (contract-internal) + +Both actions below require `require_auth(get_self())`, so they run only as inline actions dispatched by the contract itself, never as directly submitted top-level actions. They carry stable event payloads for indexers. + +### lognewlink + +- `link_id: uint64_t` +- `creator: name` +- `key: public_key` +- `asset_ids: vector` +- `memo: string` + +Sent by `announcelink`. Calls `require_recipient(creator)`. This is the only link action that directly notifies a party; the atomicassets-api indexer keys the link's initial `WAITING` row off this action's trace. + +Source: `include/atomictoolsx.hpp:65-71`, `src/link.cpp:165-174` + +### loglinkstart + +- `link_id: uint64_t` + +Sent by `receive_asset_transfer` once the assets land. Body is `require_auth(get_self())` only; it sends no `require_recipient`. It exists purely as a trace marker for indexers to move the link from `WAITING` to `CREATED` (claimable). + +Source: `include/atomictoolsx.hpp:73-75`, `src/link.cpp:176-180` + +## Admin and off-chain auth + +### init + +No parameters. + +Required authorization: the contract account itself (`require_auth(get_self())`). + +Creates the `config` singleton with its defaults (`get_or_create`) if it does not already exist, so a repeat call is a safe no-op. Run once per deployment. + +Source: `include/atomictoolsx.hpp:33`, `src/link.cpp:9-12` + +### auth + +- `nonce: string` + +Required authorization: none checked before it aborts. + +Always fails: the body is `check(false, "This action is designed to always throw")`. It exists so an off-chain service can ask a user to sign (and attempt to broadcast) an `auth` transaction with an arbitrary `nonce` as a proof-of-key-control challenge; the transaction can never commit, so it changes no state and costs no RAM, while its signature still proves the account controls its keys. Not part of the link flow. + +Source: `include/atomictoolsx.hpp:80-82`, `src/auth.cpp:7-11` + +## Internal helper + +`internal_transfer_assets(to, asset_ids, memo)` is the single path both `cancellink` and `claimlink` use to move escrowed assets out. It dispatches an inline `atomicassets::transfer` under `permission_level{get_self(), "active"}` from `atomictoolsx` to `to`. Because the sender is the contract account, if `to` has never held an AtomicAssets asset before, the `atomictoolsx` account pays the RAM for the recipient's new asset scope (AtomicAssets charges the transfer's `from` for a first-time recipient scope). It is not a standalone action. + +Source: `include/atomictoolsx.hpp:119`, `src/link.cpp:247-264` + +## Action summary + +| Action | Auth | Notifies / sends | Effect | +| --- | --- | --- | --- | +| `announcelink` | `creator` | `lognewlink` (notifies creator) | Creates a `links` row (`assets_transferred=false`), creator pays RAM | +| `receive_asset_transfer` (notify handler) | n/a (on_notify) | `loglinkstart` | Marks the matching link funded; requires memo `"link"` | +| `claimlink` | `claimer` | inline `atomicassets::transfer` (memo `"Claimed link"`) | Verifies signature, sends assets to claimer, erases row | +| `cancellink` | link `creator` | inline `atomicassets::transfer` if funded (memo `"Cancelled link"`) | Returns any escrowed assets, erases row | +| `lognewlink` | `get_self()` | `require_recipient(creator)` | Event log for indexers | +| `loglinkstart` | `get_self()` | none | Event log for indexers | +| `init` | `get_self()` | none | Creates the `config` singleton | +| `auth` | none (always aborts) | none | Off-chain key-control challenge; never commits | + +Source: `include/atomictoolsx.hpp:33-82`, `src/link.cpp`, `src/auth.cpp` diff --git a/reference/atomictools/tables.md b/reference/atomictools/tables.md new file mode 100644 index 0000000..232afb8 --- /dev/null +++ b/reference/atomictools/tables.md @@ -0,0 +1,78 @@ +--- +scope: Complete table reference for the atomictools contract (links / config) +depends-on: [reference/atomictools/actions.md] +key-modules: ["atomictools-contract (commit d89ce79e4): include/atomictoolsx.hpp, src/link.cpp"] +--- + +# AtomicTools tables + +Complete table reference for the `atomictools` contract, deployed as account `atomictoolsx`. The contract has no release tags; citations pin commit `d89ce79e4` of `pinknetworkx/atomictools-contract`. The contract defines exactly two tables, `links` and `config`, both scoped to the contract account itself. Struct and typedef citations are to `include/atomictoolsx.hpp`; behavior citations are to `src/link.cpp`. + +Live-chain status: the deployed `atomictoolsx` ABI on WAX mainnet lists exactly these two tables, and their field layouts match the pinned source. The row shapes below are copied from live `get_table_rows` reads against `wax.greymass.com`. + +## links + +Contract table name: `links`. C++ struct: `links_s`. + +Scope: `get_self()` (the `atomictoolsx` account; one shared scope for every link). + +Primary key: `link_id`. + +Secondary indexes: `assetidshash` (`checksum256`, index position 2, `key_type` `sha256`) computed by `hash_asset_ids(asset_ids)`. The hash sorts the asset ids before hashing, so it is identical for any two links holding the same id set in any order. The contract uses it to find a link by its asset set when funding a transfer and when checking for a duplicate announcement. + +| Column | Type | Meaning | +| --- | --- | --- | +| `link_id` | `uint64` | Assigned from the contract-wide `config.link_counter` (starts at 1). Also the primary key. | +| `creator` | `name` | The account that announced the link; the only account that can `cancellink` it, and the account escrowed assets are returned to on cancel. Pays the row's RAM. | +| `key` | `public_key` | The link's public key. A `claimlink` succeeds only when its signature over `sha256(claimer)` recovers to this key. The matching private key is shared out of band to whoever may claim. | +| `asset_ids` | `uint64[]` | The escrowed asset ids. Feeds the `assetidshash` index. | +| `assets_transferred` | `bool` | `false` after `announcelink`; set `true` (with `same_payer`) by the transfer notification handler once the assets land. `claimlink` requires it to be `true`; `cancellink` returns the assets only when it is `true`. | +| `memo` | `string` | Free-form, up to 256 characters. | + +A row exists from `announcelink` until the link is claimed or cancelled; both terminal actions erase it, so the `links` table holds only live (announced or funded, not yet resolved) links. Changed state is not recorded on chain beyond `assets_transferred`; the announced-versus-claimed-versus-cancelled distinction lives in the indexer (see "Reading links" in `guides/links.md`). + +Source: `include/atomictoolsx.hpp:88-102`, `src/link.cpp:21-98` (`announcelink`), `src/link.cpp:189-241` (funding), `src/link.cpp:107-159` (cancel/claim) + +Live chain example (`wax.greymass.com get_table_rows`, `code=atomictoolsx`, `scope=atomictoolsx`, `table=links`, two rows): + +```json +{"link_id":3265012,"creator":"yrgb4.wam","key":"EOS6YxGQNSuv5hk2YRjHRZN9rpvJAf323ZfRPzp22C8fy8pW8kfmB", + "asset_ids":["1099925607623"],"assets_transferred":1,"memo":""}, +{"link_id":1445742,"creator":"lqpd.wam","key":"EOS6XgMxUpjuSPtvvfDnMWyC5DHbskPoeTJpUMdXdY4Giy91nTJTh", + "asset_ids":["1099602125511","1099595841824"],"assets_transferred":1,"memo":""} +``` + +`get_table_rows` renders `key` in the legacy `EOS...` format; the hosted API renders the same key as `PUB_K1_...`. They are two encodings of one key. + +## config + +Contract table name: `config`. C++ struct: `config_s`. EOSIO `singleton`. + +Scope: `get_self()`. No user-visible primary key beyond the singleton's own fixed key. + +Secondary indexes: none. + +| Column | Type | Meaning | +| --- | --- | --- | +| `version` | `string` | Contract-reported version. Hard-coded default `1.0.0`; the contract has no action that changes it, so it reads `1.0.0` on the live deployment. | +| `link_counter` | `uint64` | Next `link_id` to assign. Default 1; incremented by every `announcelink`. | +| `atomicassets_account` | `name` | The AtomicAssets contract this deployment escrows assets through. Default `atomicassets` (`atomicassets::ATOMICASSETS_ACCOUNT`); fixed at `init` and never changed by any action. | + +The struct is exposed both as a `singleton` and, for ABI-visibility, as a one-row `multi_index` (a known eosio.cdt workaround so the singleton appears in the ABI). Created by `init`. + +Source: `include/atomictoolsx.hpp:105-116`, `src/link.cpp:9-12` (`init`), `src/link.cpp:72-74` (`link_counter` increment) + +Live chain example (`code=atomictoolsx`, `scope=atomictoolsx`, `table=config`): + +```json +{"version":"1.0.0","link_counter":6572117,"atomicassets_account":"atomicassets"} +``` + +## Table scoping summary + +| Table | Contract name | Scope | Primary key | Secondary indexes | +| --- | --- | --- | --- | --- | +| Links | `links` | `get_self()` | `link_id` | `assetidshash` (`sha256` of sorted `asset_ids`) | +| Config | `config` | `get_self()` | singleton | none | + +Source: `include/atomictoolsx.hpp:88-116` diff --git a/reference/media.md b/reference/media.md new file mode 100644 index 0000000..a0c202a --- /dev/null +++ b/reference/media.md @@ -0,0 +1,90 @@ +--- +scope: How NFT media is referenced in the Atomic ecosystem - IPFS storage, de-facto media field-name conventions, bare-CID vs URL handling, and gateway resolution +depends-on: + - reference/atomicassets/serialization.md + - reference/atomicassets/custom-types.md + - reference/atomicassets/data-precedence.md +key-modules: ["atomicassets-contract (v2.0.0-rc4): include/atomicdata.hpp (image/ipfs wire types); live reads of wax.api.atomicassets.io and public IPFS gateways"] +--- + +# Media conventions + +How an Atomic NFT points at its media, and what an integrator must do to render it. The chain stores no image bytes and no media column: media is carried as ordinary schema attributes whose values are IPFS references, under field names that are collection convention rather than contract rule. This page covers those conventions and how to resolve them. The wire encoding of the `ipfs` and `image` FORMAT types is in `reference/atomicassets/serialization.md`; the V2 `FORMAT_TYPE` media-type mechanism is in `reference/atomicassets/custom-types.md`; which data layer a given attribute comes from is in `reference/atomicassets/data-precedence.md`. This file does not restate those. + +## Media is attribute data, not a dedicated field + +An asset's image, animation, and any other media are stored as normal attributes in the same serialized data layers as every other attribute - template immutable, asset immutable, asset mutable, and (V2) template mutable data - and are read back by deserializing those layers and merging them (`reference/atomicassets/data-precedence.md`). There is no media-specific column on the `assets`, `templates`, or `collections` tables; a media attribute is distinguished only by its name and the shape of its value. Collection-level media follows the same rule: a collection's own artwork lives in its `collection_format`-serialized data under attribute names, not in a fixed schema field. + +Source: live read, `https://wax.api.atomicassets.io/atomicassets/v1/templates?collection_name=alien.worlds` (each template's `immutable_data` carries the media attributes inline alongside `name`, `rarity`, and the rest); `reference/atomicassets/data-precedence.md` for the layer merge + +## The media field names are de-facto conventions + +The contract mandates exactly one attribute - a `string` attribute literally named `name` (`reference/atomicassets/structure.md`). Every media field name is a convention a collection chose, and readers must treat these names as data, not as guaranteed keys. The widely observed conventions on WAX are: + +- `img` - the primary still image. Present across alien.worlds, farmersworld, gpk.topps, official.wax, kogsofficial, and effectively every card-style collection sampled. +- `backimg` - a reverse or back-of-card image (alien.worlds `arms.worlds`, kogsofficial `pets.v1`). +- `video` - an animation or video loop, used alongside `img` as a richer render (official.wax `walkers`, kogsofficial `pets.v1`). + +Collection artwork uses `img` for the collection logo and an `images` attribute carrying a JSON-encoded string map for named renditions - observed on official.wax as `images: "{\"banner_1920x500\":\"Qm...\",\"logo_512x512\":\"Qm...\"}"`, a JSON object serialized into a single string attribute rather than structured on-chain fields. A reader that wants the banner must parse that inner JSON itself. + +None of these names is reserved or validated, so a collection can omit them, rename them, or add others (`audio`, `model`, a `.glb` field); a renderer keyed to a fixed name list silently shows nothing for a collection that named its image something else. Discover the media fields a collection actually uses by reading its schema `format` and its data, not by assuming `img`. + +Source: live reads, `https://wax.api.atomicassets.io/atomicassets/v1/templates?collection_name=` for alien.worlds, farmersworld, gpk.topps, official.wax, kogsofficial; `https://wax.api.atomicassets.io/atomicassets/v1/collections/official.wax` (collection `img`, and `images` JSON-string map) + +## Media attributes are usually typed `image` or `string`, not `ipfs` + +The FORMAT type system has a dedicated `ipfs` type (`reference/atomicassets/custom-types.md`), but the large sampled collections do not use it for their display media. Every `img` attribute observed is declared as type `image`, and `video`/`backimg` attributes are declared as `image` or plain `string`: + +| Collection | Schema | Media attribute types (observed) | +| --- | --- | --- | +| official.wax | walkers | `img: image`, `video: string` | +| alien.worlds | arms.worlds | `img: image`, `backimg: image` | +| gpk.topps | packs | `img: image` | +| kogsofficial | pets.v1 | `img: image`, `video: string`, `backimg: string` | + +This matters because `image` and `string` share one wire encoding and carry the value as a plain UTF-8 string - `image` is only a display-convention alias for `string` (`reference/atomicassets/serialization.md`). The `ipfs` type is the one that Base58-decodes its value to raw multihash bytes on chain; the `image`/`string`-typed media fields do not, so their stored value is the exact reference string the minter supplied. Two consequences: the attribute's declared type does not reliably tell you the value is an IPFS reference (an `image`-typed field is indistinguishable at the type level from any other string), and a reader must inspect the value's shape, not the schema type, to decide how to resolve it. The one place the `ipfs` type is conventionally used is `collection_format`, where the collection `img` is typically declared `ipfs` (`reference/atomicassets/tables.md`); even there the API hands you back a Base58 string, since `ipfs` values must be re-encoded to Base58 for display (`reference/atomicassets/serialization.md`). + +Source: live reads, `https://wax.api.atomicassets.io/atomicassets/v1/schemas//` for the four rows above; `reference/atomicassets/serialization.md` (`image` == `string` wire encoding; `ipfs` Base58 round-trip) + +## Media values are bare IPFS references, in several shapes + +The stored value is a bare content reference with no URI scheme and no gateway host - not a `ipfs://` URL and not an `https://` URL. Across the sampled collections the value takes several shapes, and defensive parsing has to accept all of them: + +- **Bare CIDv0** - a Base58 `Qm...` multihash, for example `img: "QmXHzdok8Sxvhj1tPXYk7vG6hnWayFU1WnvoPpieDsh55X"` (alien.worlds). This is the dominant form. +- **Bare CIDv1** - a Base32 `bafy...` string, for example `video: "bafybeiejeflvsf3h5j7j6q5cie4andjxv6qkbnekk2hahujezm3srpn4bu"` (official.wax `walkers`). A parser that only recognizes the `Qm` prefix drops these. +- **CID plus a path suffix** - a directory CID with a trailing file path, for example `img: "QmUt1n6b5re5FhuP7dFj73BS57MGNBYPP3uZeTnvYDtiyN/FoodFightPackArt_WinterCon.png"` (gpk.topps). The suffix is part of the reference and must be preserved through resolution, not stripped to the CID. + +A minter can also store a full `https://` URL in these fields; nothing in the contract constrains the string. So a renderer must handle both a bare reference (prepend a gateway) and an already-absolute URL (use as-is), and must not assume the value begins with `Qm` or is a single path-free CID. + +Source: live reads, `https://wax.api.atomicassets.io/atomicassets/v1/templates?collection_name=` for alien.worlds, official.wax, gpk.topps + +## Resolving a reference to a fetchable URL + +The convention is a division of labor: the chain stores the bare CID (keeping the on-chain value small and gateway-agnostic), and the client chooses a gateway at render time and forms `https:///ipfs/`. Because the stored value already carries any path suffix, string concatenation onto `/ipfs/` produces the correct URL for the CID-plus-path form without special handling. If the value is already an absolute URL, resolution is a no-op - use it directly. + +`https://ipfs.io/ipfs/` is the canonical public gateway and resolves these references: a live GET of `https://ipfs.io/ipfs/QmXHzdok8Sxvhj1tPXYk7vG6hnWayFU1WnvoPpieDsh55X` (the alien.worlds template `img` above) returns HTTP 206 with `Content-Type: image/webp` and the `RIFF....WEBP` file signature, confirming the bare on-chain CID fetches real image bytes through a public gateway with no chain-side transformation. Dedicated gateways (a project-run or commercial IPFS gateway) are used in production for latency and rate-limit headroom, and a resilient client resolves through its preferred gateway first and falls back to another on failure rather than hard-coding one host. Which gateway to use, and whether the referenced content stays pinned and therefore retrievable at all, are integrator and operator concerns that this ecosystem does not settle on chain: a CID on chain is a claim about content, not a guarantee any node still serves it. + +Source: live gateway read, `GET https://ipfs.io/ipfs/QmXHzdok8Sxvhj1tPXYk7vG6hnWayFU1WnvoPpieDsh55X` (HTTP 206, `image/webp`, WebP magic bytes) + +## V2 media-type descriptors + +V2 adds an optional per-attribute descriptor - `setschematyp` writes a `FORMAT_TYPE { name, mediatype, info }` into the `schematypes` table, letting a collection tag what a media attribute holds (marking an image reference as a `.glb` model or a `.png`, for example) for downstream tooling. This is metadata only, never affects serialization or the stored value, and is covered in `reference/atomicassets/custom-types.md`. Do not infer a media type from the API's schema response: it synthesizes a default `mediatype` per attribute even when no on-chain descriptor exists, so the presence of a `mediatype` field is not evidence a real `FORMAT_TYPE` was set - read the `schematypes` table on chain to know (`reference/atomicassets/tables.md`). Absent a descriptor, the file type must be inferred from the fetched bytes (as the WebP signature above shows) or the value's path suffix, not from the attribute name. + +Source: `reference/atomicassets/custom-types.md` (FORMAT_TYPE, setschematyp), `reference/atomicassets/tables.md` (API mediatype-synthesis caveat) + +## Correct and avoid + +Correct: + +- Discover media fields from the collection's schema `format` and data; treat `img`/`backimg`/`video` as likely-but-not-guaranteed names. +- Accept every value shape: bare CIDv0 (`Qm...`), bare CIDv1 (`bafy...`), CID-plus-path, and full `https://` URLs. +- Resolve a bare reference as `https:///ipfs/` and use an already-absolute URL as-is; preserve any path suffix intact. +- Resolve through a preferred gateway with a fallback gateway on failure. +- Determine content type from the fetched bytes or a V2 `schematypes` descriptor read from chain, not from the attribute name. + +Avoid: + +- Hotlinking a single hard-coded gateway with no fallback - one gateway's outage or rate limit then blanks all media. +- Assuming `img` is always present, always a CID, or always begins with `Qm` - CIDv1, path suffixes, and full URLs all occur, and the field may be absent or renamed. +- Inferring "this is IPFS" from the attribute's FORMAT type - `image`/`string`-typed fields carry the reference as an opaque string and look identical to any other text attribute. +- Trusting the API's synthesized `mediatype` as proof a real media-type descriptor was set. diff --git a/reference/sdk/atomicassets.md b/reference/sdk/atomicassets.md new file mode 100644 index 0000000..1e56d16 --- /dev/null +++ b/reference/sdk/atomicassets.md @@ -0,0 +1,128 @@ +--- +scope: "@atomichub/atomicassets JavaScript/TypeScript SDK: ExplorerApi and RpcApi reads, attribute serialization, and v2 action building" +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 SDK + +The official JavaScript/TypeScript client for the AtomicAssets NFT standard on Antelope chains. It reads NFT 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. + +``` +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. + +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) + +## 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. + +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. + +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. + +Live reads against `https://wax.api.atomicassets.io` (WAX mainnet): + +```js +import { ExplorerApi, explorerApiForNetwork } from '@atomichub/atomicassets'; + +const api = explorerApiForNetwork('wax'); // or: new ExplorerApi('https://wax.api.atomicassets.io', 'atomicassets', {}) + +await api.getAsset('1099519850420'); +// -> { asset_id: '1099519850420', name: 'Proof of Concept 4/4', owner: '14yr4.wam', +// collection: { collection_name: 'cryptoswatch', ... }, schema: { schema_name: 'swatches', ... }, ... } + +await api.getAssets({ collection_name: 'pixeltycoons', sort: 'asset_id', order: 'desc' }, 1, 2); +// -> IAsset[] of length 2 + +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 `reference/api.md` ("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` + +## RpcApi reads chain tables directly, with a rate-limited queue and a cache + +`RpcApi` reads the AtomicAssets contract tables straight from a node's `/v1/chain/get_table_rows` rather than through the indexer. The constructor takes `(endpoint, contract, { fetch?, rateLimit? })`; `contract` defaults to `atomicassets` through the `rpcApiForNetwork` factory, and `rateLimit` defaults to 4. Requests pass through an internal queue that releases at `rateLimit` calls per second (a 250 ms interval at the default), so a burst of `getAsset` calls is spread out rather than fired at once. Resolved rows are held in an in-memory cache for 15 minutes; passing `cache = false` to a getter evicts that entry first and forces a fresh read. + +The getters return lazy wrapper objects, not plain rows. `getAsset(owner, id)` resolves an `RpcAsset` whose `immutableData()`/`mutableData()`/`data()` methods deserialize the row's `serialized_data` against the schema format fetched on demand, and whose `template()`/`collection()`/`schema()` resolve the related wrappers; `data()` applies AtomicAssets precedence (template data overrides asset data, immutable over mutable). `getTemplate`, `getCollection`, `getSchema`, `getOffer`, and the account-scoped `getAccountAssets`/`getAccountOffers`/`getCollectionInventory` follow the same wrapper pattern. `getTableRows` is the raw escape hatch; it forces `limit: 101` and `json: true`. + +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()`) + +## Serialization decodes table blobs; the attribute-map helpers build action data + +Two distinct jobs use two distinct helpers, and mixing them is a common error. `ObjectSchema(format)` builds a codec from a schema format (an array of `{ name, type }`), and `serialize(object, codec)` / `deserialize(bytes, codec)` convert between a plain object and the binary `serialized_data` that chain rows and SHIP deltas carry. `toByteArray(input)` normalizes the three shapes serialized data arrives in (a hex string, optionally `\x`-prefixed from Postgres bytea; a plain number array; or a `Uint8Array`) before decoding. `CachedObjectSchema` memoizes the codec by the JSON of its format, bounded to 500 entries, for hot paths that rebuild the same schema per row. This is the read path: it is what `RpcAsset.immutableData()` uses under the hood, and what a filler uses to decode deltas. For the binary format itself see `reference/atomicassets/serialization.md`. + +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. + +Round-trip run against a live schema format read from `https://wax.api.atomicassets.io`, and a standalone format: + +```js +import { ObjectSchema, serialize, deserialize } from '@atomichub/atomicassets'; + +const format = [{ name: 'name', type: 'string' }, { name: 'level', type: 'uint32' }, { name: 'img', type: 'ipfs' }]; +const codec = ObjectSchema(format); +const obj = { name: 'Hero', level: 42, img: 'QmABC' }; +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` + +## 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. + +```js +import { ActionBuilder, createAttributeMap } from '@atomichub/atomicassets'; + +const builder = new ActionBuilder('atomicassets'); +const immutable = createAttributeMap({ name: 'Hero' }, { name: 'string' }); +builder.mintasset('minteracct', 'pixeltycoons', 'heroes', 4, 'targetacct', immutable, [], []); +// -> { account: 'atomicassets', name: 'mintasset', +// data: { authorized_minter: 'minteracct', collection_name: 'pixeltycoons', schema_name: 'heroes', +// template_id: 4, new_asset_owner: 'targetacct', +// 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 + +## 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 + +## 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. + +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) + +## 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 `reference/wharfkit.md`. + +Source: atomicassets-sdk (main, 80580c5) src/Actions/Generator.ts (`EosioActionObject`, `EosioAuthorizationObject`, `_authorize`) + +## When to use the SDK versus raw HTTP or wharfkit table reads + +Three read paths cover different needs, consistent with `guides/querying-the-api.md`: + +- **`ExplorerApi` (this SDK)** for typed indexer reads from JS/TS: filtered lists, search, sort, counts, stats, and cross-owner enumeration, with params and response objects already typed and errors raised as `ApiError`. This is the default for application and bot code. +- **Raw HTTP against the atomicassets-api** when you are not in a JS runtime, or want full control over paging and caching without the wrapper. The endpoints, the limit cap, and the lifecycle-state enums are in `reference/api.md`. +- **Chain table reads** (`RpcApi` here, or `@wharfkit/antelope`'s `get_table_rows`) when you need unindexed chain truth without indexer lag, or a node is your only backend. Note the numeric-key and `show_payer` pitfalls of the typed wharfkit client in `reference/wharfkit.md`; `RpcApi` reads through its own queue and cache instead. + +List endpoints reached through `ExplorerApi` inherit the deployment's `limit` cap of 100; see `reference/api.md` ("List endpoints cap limit at 100"). diff --git a/reference/sdk/atomicmarket.md b/reference/sdk/atomicmarket.md new file mode 100644 index 0000000..3f76aeb --- /dev/null +++ b/reference/sdk/atomicmarket.md @@ -0,0 +1,117 @@ +--- +scope: "@atomichub/atomicmarket JavaScript/TypeScript SDK: AtomicMarketApi reads, the v2 royalty read layer, and royalty-config action building" +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 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. + +``` +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. + +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) + +## 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 `reference/api.md` ("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. + +Live reads against `https://wax.api.atomicassets.io` (WAX mainnet): + +```js +import { AtomicMarketApi, marketApiForNetwork } from '@atomichub/atomicmarket'; + +const market = marketApiForNetwork('wax'); + +await market.getSales({ state: '3', sort: 'sale_id', order: 'desc' }, 1, 2); +// -> ISale[] of length 2 + +await market.getSale('173548902'); +// -> { sale_id: '173548902', seller: 'alienz251212', state: 3, +// price: { amount: '992994', token_symbol: 'WAX', ... }, collection: { collection_name: 'rustveil', ... }, ... } + +await market.getConfig(); +// -> { maker_market_fee: 0.01, taker_market_fee: 0.01, version: '1.3.3', ... } +``` + +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` + +## The v2 royalty read layer returns config, template rules, and attribute rules + +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`. + +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`. + +Live reads against the WAX testnet deployment `https://test.wax.api.atomicassets.io`: + +```js +const test = marketApiForNetwork('wax-testnet'); + +await test.getRoyaltyConfig('royaltycol11'); +// -> { collection_name: 'royaltycol11', +// founders: [{ weight: 1, recipient: 'jacktestr125' }, { weight: 3, recipient: 'pe2etestacct' }], +// attribute_mode: 0, split_founders: '2', split_templates: '1', split_attributes: '1', ... } + +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' }], ... }] +``` + +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/` + +## MarketActionBuilder builds the v2 royalty-config actions + +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. + +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. + +```js +import { MarketActionBuilder } from '@atomichub/atomicmarket'; + +const builder = new MarketActionBuilder('atomicmarket'); +builder.setroyalconf('mycollection', { + founders: [{ recipient: 'founderacct', weight: 10000 }], + attribute_mode: 0, split_founders: 5000, split_templates: 3000, split_attributes: 2000 +}); +// -> [{ account: 'atomicmarket', name: 'setroyalconf', +// data: { collection_name: 'mycollection', founders: [{ recipient: 'founderacct', weight: 10000 }], +// attribute_mode: 0, split_founders: 5000, split_templates: 3000, split_attributes: 2000 } }] + +// Weights and splits are relative shares whose semantics (and how they resolve +// to payouts of the collection fee) are defined in +// reference/atomicmarket/fees-and-royalties.md; read that page before +// configuring a live collection, since a wrong split misdirects royalty payouts. +builder.settemplroy('mycollection', 12345, [{ recipient: 'artistacct', weight: 10000 }]); +// -> [{ account: 'atomicmarket', name: 'settemplroy', +// 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 + +## 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 + +## 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 `reference/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 `reference/wharfkit.md`, and that large ids can arrive as strings. + +List endpoints reached through `AtomicMarketApi` inherit the deployment's `limit` cap of 100; see `reference/api.md` ("List endpoints cap limit at 100"). diff --git a/validation-log.md b/validation-log.md index 0d06792..c41d2eb 100644 --- a/validation-log.md +++ b/validation-log.md @@ -12,19 +12,28 @@ This log traces how every fact in `reference/` and `guides/` was checked before - `atomicassets-contract` at `v2.0.0-rc4` - `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) +- `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) +- `@atomichub/vert` at `2.2.0`, commit `a8a4160` A page's `key-modules` frontmatter names the specific baseline(s) it draws from; entries below carry the same pin unless noted otherwise. ## Caveat: V2 is live on WAX testnet, not on WAX mainnet -WAX mainnet still runs the V1 `atomicassets` and `atomicmarket` contracts (confirmed by live `get_abi` and `get_table_rows` reads; see `reference/atomicassets/v2-upgrade.md`). WAX testnet runs the full V2 contracts: the V2 tables are present and populated, and jungle4 carries the V2 code with unseeded tables. Many V2 contract-behavior facts in this repository are therefore still tiered `source-read` because they were written from the rc4/rc2 source, but the create, trade, and read integrator flows have now been executed end-to-end against live V2 on wax-testnet (the cold-validation harness, `packages/testnet-e2e/cold-validation.md`): a full asset lifecycle, a sale settling founders/template/attribute royalties that sum exactly to the collection fee, and a full read reconstruction over both the API and chain tables. Facts confirmed by those runs are tiered `live-chain`/`both` and noted per page below. A `source-read` tier means only that chain execution has not confirmed that specific fact, not lesser confidence in the reading. Hosted-API facts (pagination, lifecycle states) and chain-RPC facts (error codes, serialization) are checked against the live deployment and nodeos; V1 and V2 share the indexer and RPC layer for everything those facts describe. +WAX mainnet still runs the V1 `atomicassets` and `atomicmarket` contracts (confirmed by live `get_abi` and `get_table_rows` reads; see `reference/atomicassets/v2-upgrade.md`). WAX testnet runs the full V2 contracts: the V2 tables are present and populated, and jungle4 carries the V2 code with unseeded tables. Many V2 contract-behavior facts in this repository are therefore still tiered `source-read` because they were written from the rc4/rc2 source, but the create, trade, and read integrator flows have been executed end-to-end against live V2 on wax-testnet (the cold-validation harness, `packages/testnet-e2e/cold-validation.md`): a full asset lifecycle, a sale settling founders/template/attribute royalties that sum exactly to the collection fee, and a full read reconstruction over both the API and chain tables. Facts confirmed by those runs are tiered `live-chain`/`both` and noted per page below. A `source-read` tier means only that chain execution has not confirmed that specific fact, not lesser confidence in the reading. Hosted-API facts (pagination, lifecycle states) and chain-RPC facts (error codes, serialization) are checked against the live deployment and nodeos; V1 and V2 share the indexer and RPC layer for everything those facts describe. ## Pages | 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). | +| `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-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/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. | @@ -48,13 +57,16 @@ WAX mainnet still runs the V1 `atomicassets` and `atomicmarket` contracts (confi | `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/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/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 -20 source-read, 1 live-chain, 6 both. 27 pages total. +22 source-read, 1 live-chain, 13 both. 36 pages total. ## Pages with an ambiguous tier signal