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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 23 additions & 18 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -299,21 +299,20 @@ jobs:
with:
node-version: '24'

# No key reaches this step. A starter that signs reads its two
# variables, finds neither, and exits zero naming the one it wanted,
# which is what a reader who cloned without keys sees too.
- name: Starters that need no key
# No key reaches this step, which is why it runs on every event. The
# two read-only starters exercise the live API here; the three
# signing ones prove the path a reader who cloned without keys
# takes, exiting zero and naming the variable they wanted. The
# install also leaves node_modules in place for the arm below.
- name: Every starter's tests, with no key in scope
run: |
if [ ! -d starters ]; then
echo "No starters/ directory yet. This arm starts running with the pull request that lands it."
exit 0
fi
set -euo pipefail

for starter in starters/*/; do
echo "::group::${starter}"
(
cd "${starter}"
if [ -f package-lock.json ]; then npm ci --no-audit --no-fund; else npm install --no-audit --no-fund; fi
npm ci --no-audit --no-fund
node --test
)
echo "::endgroup::"
Expand All @@ -328,27 +327,33 @@ jobs:
# `github.event.pull_request.head.repo.full_name == github.repository`
# beside it, or the keys follow the workflow into a fork's pull
# request.
#
# The three run in this order because each needs what the one before
# it wrote: mint-asset mints into a collection create-collection
# made, and list-a-sale lists what mint-asset minted.
- name: Starters that sign on WAX testnet
if: github.event_name == 'push' || github.event_name == 'schedule'
env:
WAX_TESTNET_ACTOR: ${{ secrets.WAX_TESTNET_ACTOR }}
WAX_TESTNET_PRIVATE_KEY: ${{ secrets.WAX_TESTNET_PRIVATE_KEY }}
run: |
if [ ! -d starters ]; then
echo "No starters/ directory yet. This arm starts running with the pull request that lands it."
exit 0
fi
set -euo pipefail

if [ -z "${WAX_TESTNET_ACTOR}" ] || [ -z "${WAX_TESTNET_PRIVATE_KEY}" ]; then
# Defaulted rather than read bare. The step env always defines
# both keys, but an unset one under `set -u` would abort the
# arm before it could report why.
if [ -z "${WAX_TESTNET_ACTOR:-}" ] || [ -z "${WAX_TESTNET_PRIVATE_KEY:-}" ]; then
echo "::warning::WAX_TESTNET_ACTOR or WAX_TESTNET_PRIVATE_KEY is unset, so each signing starter skips itself"
fi

for starter in starters/*/; do
# Named rather than globbed. The order is load-bearing, and a
# read-only starter run here would repeat what the arm above
# already proved.
for starter in create-collection mint-asset list-a-sale; do
echo "::group::${starter}"
(
cd "${starter}"
if [ -f package-lock.json ]; then npm ci --no-audit --no-fund; else npm install --no-audit --no-fund; fi
node --test
cd "starters/${starter}"
node src/index.js
)
echo "::endgroup::"
done
9 changes: 8 additions & 1 deletion .markdownlint-cli2.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@
// everything else is a preference this corpus does not hold.
{
"globs": ["**/*.md"],
"ignores": ["node_modules", ".git", ".github/PULL_REQUEST_TEMPLATE.md"],

// Recursive, not top-level. Each starter installs its dependencies into
// its own node_modules, and a bare "node_modules" would leave every
// dependency's README under this rule set the moment a contributor runs
// the tool after an install. Continuous integration never sees them,
// because the job that runs this installs nothing, so the mismatch would
// land only on the reader running it locally.
"ignores": ["**/node_modules", "**/.git", ".github/PULL_REQUEST_TEMPLATE.md"],
"config": {
// Only the rules below run.
"default": false,
Expand Down
10 changes: 9 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Start here. Find the outcome you are working toward below, read the file on that

| To do this | Read |
| --- | --- |
| Go from an empty testnet account to a minted asset, one step at a time | `tutorials/first-collection.md` |
| Clone runnable code for a read, a mint, or a listing | `tutorials/starters.md` |
| Weigh AtomicAssets against the single-token standard on an EVM chain | `concepts/compared-with-erc721.md` |
| Choose between a chain table read and the hosted API before writing the reader | `concepts/reading-atomic-data.md` |
| Read assets, sales, and collections over HTTP, with no key and no account | `guides/querying-the-api.md` |
| Look up one hosted-API endpoint: pagination caps, lifecycle states, rate limits, the two sales routes | `reference/api.md` |
| Classify a chain RPC error, and tell a missing account apart from a broken node | `reference/chain.md` |
Expand All @@ -20,8 +24,10 @@ Start here. Find the outcome you are working toward below, read the file on that
| Read a table or check an authority through the client library, or migrate off eosjs | `reference/wharfkit.md` |
| Subscribe to live asset, sale, and auction events over Socket.IO | `reference/api-streaming.md` |
| Show an asset's image or video from what the chain actually stores | `reference/media.md` |
| Say what the chain guarantees about an owner, and what it does not guarantee about media | `concepts/ownership-on-chain.md` |
| Sign anything: build the session, pick the chain, install the signer | `guides/signing.md` |
| Mint an asset on testnet, from collection and schema through transfer and burn | `guides/asset-lifecycle.md` |
| Work out why the data is split across four levels, and what a level below inherits | `concepts/four-level-model.md` |
| Work out what a collection, a schema, a template, and an asset each own | `reference/atomicassets/structure.md` |
| Look up one AtomicAssets action: parameters, authorization, RAM payer, V2 changes | `reference/atomicassets/actions.md` |
| Read or index one AtomicAssets table row | `reference/atomicassets/tables.md` |
Expand All @@ -35,10 +41,12 @@ Start here. Find the outcome you are working toward below, read the file on that
| Offer to buy an asset nobody has listed, or accept such an offer | `guides/buyoffers.md` |
| Swap assets with another account without going through the market | `guides/offers.md` |
| Fund a buyer's balance, or find out why a payout is still parked | `guides/deposits.md` |
| Say why a resale royalty is paid at settlement rather than requested of a storefront | `concepts/royalties.md` |
| Work out what a settlement actually pays: fee stack, royalty splits, the dust rule | `reference/atomicmarket/fees-and-royalties.md` |
| Look up one AtomicMarket action | `reference/atomicmarket/actions.md` |
| Read or index one AtomicMarket table row | `reference/atomicmarket/tables.md` |
| Find what V2 changed on the market, bundle retirement and the guards included | `reference/atomicmarket/v2-changes.md` |
| Work out why every listing lands in one contract, and how a storefront still earns | `concepts/one-order-book.md` |
| Run a storefront that earns maker and taker fees | `reference/atomicmarket/marketplaces.md` |
| Size RAM for a busy marketplace, and find out who pays for which row | `reference/atomicmarket/ram.md` |
| Hand assets to someone who has no account yet, through a claim link | `guides/links.md` |
Expand All @@ -54,7 +62,7 @@ Start here. Find the outcome you are working toward below, read the file on that

Read the routed file in full rather than searching it. The facts there encode behavior no method signature carries: which account is billed for a row, which read answers with a null instead of an error, which flag lets a transaction commit while delivering nothing.

Take each section at face value and do not extrapolate past what it states. A page says what was checked, and what it does not say was not checked. `validation-log.md` records how every page was validated and against what.
Take each section at face value and do not extrapolate past what it states. A page says what was checked, and what it does not say was not checked. `validation-log.md` records how every page was validated and against what. It grades `reference/` and `guides/` only: a tutorial's claim is that its steps run, and a concepts page restates facts the pages it links already carry, so neither takes a row there.

Re-check any fact that names a version when that dependency moves. The two SDK pages are pinned to `@atomichub/atomicassets` 2.1.1, read at tag `v2.1.1`, and `@atomichub/atomicmarket` 2.4.1, read at tag `v2.4.1`; the client-library page is pinned to `@wharfkit/antelope` 1.1.1, and the AtomicAssets and AtomicMarket contract pages to `v2.0.0-rc4` and `v2.0.0-rc2`. A fact read at one of those pins is a fact about that release, not about the package name.

Expand Down
53 changes: 53 additions & 0 deletions concepts/compared-with-erc721.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
scope: "How AtomicAssets and ERC-721 differ by mechanism: where attribute data lives, who deploys a contract, what a mint costs, and how a resale royalty is applied"
depends-on: [reference/atomicassets/structure.md, reference/atomicassets/tables.md, reference/atomicassets/actions.md, reference/media.md, reference/atomicmarket/fees-and-royalties.md]
key-modules: []
---

# AtomicAssets next to ERC-721

Both designs let an account own a unique item and prove it on chain. They diverge on four mechanisms, and the differences are consequences of those, not of anyone being careless.

Read this as two halves. Everything about AtomicAssets links to the page in this repository that validated it. Everything about ERC-721 is what that standard and its common extensions specify, which this repository has not validated and does not cite.

## Where the attributes live

ERC-721 keeps the owner of each id on chain and puts the description of the item behind a pointer. Its metadata extension defines `tokenURI`, a per-id string returning a URI, and the document at that URI holds the name, the image reference, and the traits. The contract does not read that document and cannot check it.

AtomicAssets stores attributes on chain, as fields on contract tables. A template carries `immutable_serialized_data`, an asset carries its own immutable and mutable data, and a schema declares the names and types those bytes decode against. See [AtomicAssets data model structure](../reference/atomicassets/structure.md) and [AtomicAssets tables](../reference/atomicassets/tables.md#assets).

The honest limit is that media is a pointer on both sides. The chain stores no image bytes and has no media column: a media attribute is an ordinary schema attribute whose value is a bare content reference, and a CID on chain is a claim about content rather than a guarantee any node still serves it. See [Media conventions](../reference/media.md). What differs is the rest of it. A trait an application reads is a chain read on one side and a fetch of somebody's document on the other.

## Who deploys a contract

Under ERC-721 a collection is a contract. Each project deploys its own, at its own address, with its own code, and every integrator adds that address before it can show anything from it.

Under AtomicAssets a collection is a row. The `collections` table is scoped to the contract's own account, template and asset ids come from counters that run across the whole contract, and the format that collection data serializes against is one process-wide value rather than one per collection. Creating a collection is an action, not a deployment, and it needs no code review because there is no new code. See [AtomicAssets data model structure](../reference/atomicassets/structure.md#collections).

The collection is still the unit of control. Its `author` and its `authorized_accounts` list decide who may create and edit schemas, templates, and assets under it, and its `market_fee` is the collection's own number. See [AtomicAssets data model structure](../reference/atomicassets/structure.md#authorization-and-the-24-account-cap).

The trade goes both ways. A shared contract means an indexer, a signer, or a storefront that reads one collection reads all of them, and an upgrade that lands reaches every collection at once. It also means a collection cannot change the rules for itself: custom behavior belongs in a contract of your own that reacts to notifications rather than in a fork of this one.

## What a mint costs

An ERC-721 mint costs gas, priced by demand for block space at the moment it runs, spent and not returned.

An AtomicAssets mint costs storage. `mintasset` creates the asset row and bills the RAM to `authorized_minter`, not to the recipient, so a minter without enough RAM blocks its own mint whatever the recipient holds. See [AtomicAssets actions](../reference/atomicassets/actions.md#mintasset).

Two properties follow from storage rather than gas. The bill is a stake rather than a burn: the row's RAM is released when the row is erased, and `burnasset` erases it. And the bill is transferable. The `ram_payer` field is independent of the owner: it moves to an authorized editor on `setassetdata`, the V2 reassignment actions move it deliberately, and `payofferram` lets a service take over an offer's RAM so its users do not have to hold any.

The template level exists for this reason. Data shared by many assets is stored and paid for once instead of once per asset, which is a saving with no counterpart when every mint writes its own record.

## How a resale royalty is applied

ERC-2981 gives a contract a `royaltyInfo` view that returns a recipient and an amount for a given sale price. It is a signal. The token contract does not run it during a transfer and cannot make a payment happen, so whether the recipient is paid depends on the marketplace that settles the trade choosing to read it and act on it.

AtomicMarket does the arithmetic itself. Every sale, auction claim, and buyoffer acceptance routes through one payout function that deducts the maker marketplace fee, the taker marketplace fee, the collection fee, and any active bonus fees, credits each recipient, and gives the seller the remainder. The collection's rate is read from the collection row at settlement, not taken from the listing. See [AtomicMarket fees and royalties](../reference/atomicmarket/fees-and-royalties.md#every-settlement-stacks-four-fee-layers-before-the-seller-is-paid).

The boundary matters as much as the mechanism. The arithmetic lives in AtomicMarket, and it is settling through AtomicMarket that applies it. AtomicAssets stores the collection's `market_fee` and never spends it: `transfer` moves the asset and deducts nothing, so two accounts trading directly pay no royalty. What the design buys is that a seller cannot choose a storefront that pays the collection less, because the number is not the storefront's to apply. See [Royalties are settlement math](royalties.md).

## Next

- [Why an asset has four levels](four-level-model.md) is the data model this page keeps referring to.
- [Royalties are settlement math](royalties.md) is the settlement side in full.
- [Mint your first asset on testnet](../tutorials/first-collection.md) is the shortest way to see the difference rather than read about it.
Loading
Loading