Page MenuHomePhabricator

[Cashtab] Support minting NFTs
ClosedPublic

Authored by bytesofman on Mon, Apr 22, 23:44.

Details

Reviewers
emack
Group Reviewers
Restricted Project
Commits
rABC9e9999a38f4b: [Cashtab] Support minting NFTs
Summary

Support minting SLP1 NFT Child tokens, aka "NFTs"

  • User can create "fan-out" txs to have qty-1 utxos available to mint NFTs
  • User can mint NFTs
  • NFTs are rendered with icon and some available info on the NFT Collection token action page
  • An info notice is rendered on the NFT's token action page (we do not yet support NFT actions, like sending an NFT. Will come in a later diff).
  • We parse NFT parent "fan-out" txs in tx history
  • We parse NFT mints in tx history
  • We start parsing "self-send" txs uniquely in tx history, for NFT-related txs and all txs
Test Plan

npm test

npm start, make a collection, make some NFTs. note that, as you mint NFTs, they appear in the "NFTs in this Collection" table

This diff is deployed at https://cashtab-local-dev.netlify.app

Diff Detail

Repository
rABC Bitcoin ABC
Lint
Lint Not Applicable
Unit
Tests Not Applicable

Event Timeline

Failed tests logs:

====== CashTab Unit Tests: <Token /> available actions rendered SLP1 NFT Parent token ======
TestingLibraryElementError: Unable to find an element with the text: ℹ️ Cashtab support for minting NFTs is coming soon. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.

Ignored nodes: comments, script, style
<body>
  <div>
    <div
      class="sc-gldTML iNMdzt"
    >
      <div
        class="Toastify"
      />
      <div
        class="sc-feryYK nEpHl"
      >
        <div
          class="sc-ccSCjj gsEYhs"
        >
          <div
            class="sc-cJOK ikTwje"
          >
            <div
              class="sc-cZBZkQ bktgTF"
            >
              <img
                alt="cashtab"
                class="sc-ecaExY eDawFS"
                src="test-file-stub"
              />
            </div>
            <div
              class="sc-jMMfwr dPFjyb"
              title="Wallet Info"
            >
              <div
                class="sc-hcmgZB eBXvth"
              >
                <select
                  class="sc-dHmInP dVZhbi"
                  id="wallets"
                  name="wallets"
                >
                  <option
                    class="sc-ejGVNB ekyrgz"
                    value="Token Test"
                  >
                    Token Test
                  </option>
                </select>
                <div
                  class="sc-kcbnda fpeoHR"
                >
                  <button
                    aria-label="Copy ecash:qqq9f9z3uhpzkxrgdjkd7dxuuey7tmpmugpmnw0kue"
                    class="sc-fOKMvo fAHLVb"
                  >
                    <svg
                      title="copy-paste"
                    />
                  </button>
                  <div
                    class="sc-gHboQg fjXpEN"
                  >
                    <div
                      class="sc-eilVRo impULA"
                    >
                      <input
                        checked=""
                        class="sc-dymIpo dhyqmP"
                        id="show-hide-balance"
                        name="show-hide-balance"
                        title="show-hide-balance"
                        type="checkbox"
                      />
                      <label
                        class="sc-eerKOB NZoeG"
                        for="show-hide-balance"
                      >
                        <span
                          class="sc-emmjRN gmaaWK"
                          data-off=""
                          data-on=""
                        />
                        <span
                          class="sc-cpmLhU lcLQqB"
                        />
                      </label>
                    </div>
                  </div>
                </div>
              </div>
              <div
                class="sc-eLdqWK hezRsW"
                title="Balance in XEC"
              >
                9,970.81
                 
                XEC
                 
              </div>
              <div
                class="sc-iiUIRa bMBGif"
                title="Balance in Local Currency"
              >
                $
                0.30
                 
                USD
              </div>
              <p
                class="sc-hgRTRy dwvXB"
                title="Price in Local Currency"
              >
                1 
                XEC
                 = 
                0.00003000
                 
                USD
              </p>
            </div>
          </div>
          <div
            class="sc-hRmvpr gAPCGr"
          >
            <div
              class="sc-iGrrsa gzzmZH"
            >
              100
               
              ABC Blocks
               (
              ABC
              )
            </div>
            <div
              class="sc-LKuAh blroTx"
              title="Token Stats"
            >
              <div
                class="sc-hzNEM jXJJTL"
              >
                <button
                  class="sc-jtggT eFkNSm"
                >
                  <img
                    alt="icon for 0c66493127382882053f3eb6e2e05eccff7f67378ebf5e84660a958656a304cc"
                    height="128"
                    src="https://icons.etokens.cash/128/0c66493127382882053f3eb6e2e05eccff7f67378ebf5e84660a958656a304cc.png"
                    width="128"
                  />
                </button>
              </div>
              <div
                class="sc-hzNEM jXJJTL"
              >
                <div
                  class="sc-chbbiW ifZBos"
                >
                  <div
                    class="sc-kxynE hEytpy"
                  >
                    Type:
                  </div>
                  <div
                    class="sc-hzNEM jXJJTL"
                  >
                    <div
                      class="sc-dEoRIm cJNdNO"
                    >
                      SLP NFT Collection
                       
                      <button
                        aria-label="Click for more info abou...
    at Object.getElementError (/work/cashtab/node_modules/@testing-library/dom/dist/config.js:37:19)
    at /work/cashtab/node_modules/@testing-library/dom/dist/query-helpers.js:76:38
    at /work/cashtab/node_modules/@testing-library/dom/dist/query-helpers.js:52:17
    at /work/cashtab/node_modules/@testing-library/dom/dist/query-helpers.js:95:19
    at Object.getByText (/work/cashtab/src/components/Etokens/__tests__/TokenActions.test.js:257:20)

Each failure log is accessible here:
CashTab Unit Tests: <Token /> available actions rendered SLP1 NFT Parent token

render fan-out txs and nft mint txs in history, add tests for these cases, update token actions test for nft parent, disable burns for nft parent, msgs explaining why some actions are disabled if they are, conditional rendering of token table depending on token type e.g. no decimals for NFTs

Support rendering NFTs for a collection

bytesofman edited the test plan for this revision. (Show Details)

Failed tests logs:

====== CashTab Unit Tests: <Token /> available actions rendered SLP1 NFT Parent token ======
TypeError: Cannot read properties of undefined (reading 'slice')
    at MockChronikClient.slice [as getTxHistory] (/work/modules/mock-chronik-client/index.js:337:40)
    at Object.getTxHistory [as history] (/work/modules/mock-chronik-client/index.js:318:37)
    at history (/work/cashtab/src/chronik/index.js:546:10)
    at getNfts (/work/cashtab/src/components/Etokens/Token/index.js:292:66)
    at getNfts (/work/cashtab/src/components/Etokens/Token/index.js:315:13)
    at commitHookEffectListMount (/work/cashtab/node_modules/react-dom/cjs/react-dom.development.js:23150:26)
    at commitPassiveMountOnFiber (/work/cashtab/node_modules/react-dom/cjs/react-dom.development.js:24931:11)
    at commitPassiveMountEffects_complete (/work/cashtab/node_modules/react-dom/cjs/react-dom.development.js:24891:9)
    at commitPassiveMountEffects_begin (/work/cashtab/node_modules/react-dom/cjs/react-dom.development.js:24878:7)
    at commitPassiveMountEffects (/work/cashtab/node_modules/react-dom/cjs/react-dom.development.js:24866:3)
    at flushPassiveEffectsImpl (/work/cashtab/node_modules/react-dom/cjs/react-dom.development.js:27039:3)
    at flushPassiveEffects (/work/cashtab/node_modules/react-dom/cjs/react-dom.development.js:26984:14)
    at /work/cashtab/node_modules/react-dom/cjs/react-dom.development.js:26769:9
    at workLoop (/work/cashtab/node_modules/scheduler/cjs/scheduler.development.js:266:34)
    at flushWork (/work/cashtab/node_modules/scheduler/cjs/scheduler.development.js:239:14)
    at performWorkUntilDeadline (/work/cashtab/node_modules/scheduler/cjs/scheduler.development.js:533:21)
    at Timeout.task [as _onTimeout] (/work/cashtab/node_modules/jsdom/lib/jsdom/browser/Window.js:520:19)
    at listOnTimeout (node:internal/timers:573:17)
    at processTimers (node:internal/timers:514:7)

Each failure log is accessible here:
CashTab Unit Tests: <Token /> available actions rendered SLP1 NFT Parent token

add integration tests for fan-out tx and nft mint tx

Tail of the build log:

Installing mock-chronik-client dependencies...
/work/modules/mock-chronik-client /work/abc-ci-builds/cashtab-tests

added 236 packages, and audited 237 packages in 2s

35 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
/work/cashtab /work/modules/mock-chronik-client /work/abc-ci-builds/cashtab-tests
npm ERR! code ECONNRESET
npm ERR! errno ECONNRESET
npm ERR! network Invalid response body while trying to fetch https://registry.npmjs.org/@typescript-eslint%2fexperimental-utils: aborted
npm ERR! network This is a problem related to network connectivity.
npm ERR! network In most cases you are behind a proxy or have bad network settings.
npm ERR! network 
npm ERR! network If you are behind a proxy, please make sure that the
npm ERR! network 'proxy' config is set properly.  See: 'npm help config'

npm ERR! A complete log of this run can be found in: /root/.npm/_logs/2024-04-23T23_01_44_103Z-debug-0.log
Build cashtab-tests failed with exit code 1
bytesofman edited the test plan for this revision. (Show Details)
bytesofman added inline comments.
cashtab/src/chronik/index.js
537 ↗(On Diff #47392)

For now, the best way to get all the NFTs of a given NFT collection is

  • get tx history by tokenId of parent
  • pick out the NFT mints for the NFT tokenIds

Should be indexable for easier fetching later. But, not a high priority since it is possible to do with available methods.

cashtab/src/components/Etokens/MintNftForm/index.js
1 ↗(On Diff #47392)

This is almost the same as CreateTokenForm, but I felt like that was getting a bit overloaded. NFTs have some unique input qualities. imo easier to maintain with its own component and own test, especially as we do not navigate directly do this component (it is embedded as an action on NFT Parent token action page).

cashtab/src/components/Etokens/Token/index.js
786 ↗(On Diff #47392)

the UX around NFT is, unfortunately, pretty complicated.

Will be interesting to see how they are used in Cashtab. Most successful NFT projects have a team of devs that spoonfeed a "BUY NOW" button to consumers.

This is probably the most successful recipe. But, having it implemented in Cashtab could inspire such a team of devs, provide them with working and tested code for how this can be done.

cashtab/src/components/Etokens/fixtures/mocks.js
1106 ↗(On Diff #47392)

we need a real wif now that we are testing tx creation with this wallet. use already-burned one from Transaction Fixtures

cashtab/src/components/Home/Tx/index.js
817 ↗(On Diff #47392)

new icon for self-send txs

889 ↗(On Diff #47392)

we do not render an XEC quantity or a fiat price for self-send txs

emack requested changes to this revision.EditedWed, Apr 24, 11:32
emack added a subscriber: emack.

Minting inputs on a newly created fixed supply collection.

image.png (947×1 px, 320 KB)

cashtab/src/components/Etokens/MintNftForm/index.js
246 ↗(On Diff #47393)

It returns error upon catching an exception but when everything's fine nothing's returned (i.e. undefined or null). Is this intended?

cashtab/src/components/Etokens/Token/index.js
1051 ↗(On Diff #47393)

Review flag for me to come back to this section once the minting issue above is clarified

cashtab/src/components/Home/Tx/index.js
496–501 ↗(On Diff #47393)

since the sending/burning implementation comes next and will be referencing these IDs, might be worth plonking them into config.

This revision now requires changes to proceed.Wed, Apr 24, 11:32

Per tg chat, here's the trace:

image.png (183×337 px, 21 KB)

hex: 0200000002e6410089e84b4fe22d2baf00b29f1aaf2d45b56a84491fb946aa9fb737d9a2e4010000006b483045022100f1bba9bac4356882f91309fad8ecd7d5303317ad5f7c4345baeee89d6eea222302206f96083beda6b821bcbf164301b2fa390ff3104f1b69d63ce3201d54bc2b0a86412102a607da95600985319cc9009d0970a92161c8b7cd5c8e04ac67e203cd64383600ffffffff34f4135763d899ecaf4a74c349a30d266fd733112178fe1a580a752b22817fa3020000006b483045022100d1879f63ee2974f1edb56335bfd84a67425e1e5b0ca120da02596c31d225ec3102204c175b67bffdfddac5ff66f543fa4315688cf7e962d11c521500d41aa8253dd1412102a607da95600985319cc9009d0970a92161c8b7cd5c8e04ac67e203cd64383600ffffffff150000000000000000d96a04534c500001810453454e4420e4a2d937b79faa46b91f49846ab5452daf1a9fb200af2b2de24f4be8890041e608000000000000000108000000000000000108000000000000000108000000000000000108000000000000000108000000000000000108000000000000000108000000000000000108000000000000000108000000000000000108000000000000000108000000000000000108000000000000000108000000000000000108000000000000000108000000000000000108000000000000000108000000000000000108000000000000005122020000000000001976a914a8be64f2249615f5abbe216764b386e9c49d9bc288ac22020000000000001976a914a8be64f2249615f5abbe216764b386e9c49d9bc288ac22020000000000001976a914a8be64f2249615f5abbe216764b386e9c49d9bc288ac22020000000000001976a914a8be64f2249615f5abbe216764b386e9c49d9bc288ac22020000000000001976a914a8be64f2249615f5abbe216764b386e9c49d9bc288ac22020000000000001976a914a8be64f2249615f5abbe216764b386e9c49d9bc288ac22020000000000001976a914a8be64f2249615f5abbe216764b386e9c49d9bc288ac22020000000000001976a914a8be64f2249615f5abbe216764b386e9c49d9bc288ac22020000000000001976a914a8be64f2249615f5abbe216764b386e9c49d9bc288ac22020000000000001976a914a8be64f2249615f5abbe216764b386e9c49d9bc288ac22020000000000001976a914a8be64f2249615f5abbe216764b386e9c49d9bc288ac22020000000000001976a914a8be64f2249615f5abbe216764b386e9c49d9bc288ac22020000000000001976a914a8be64f2249615f5abbe216764b386e9c49d9bc288ac22020000000000001976a914a8be64f2249615f5abbe216764b386e9c49d9bc288ac22020000000000001976a914a8be64f2249615f5abbe216764b386e9c49d9bc288ac22020000000000001976a914a8be64f2249615f5abbe216764b386e9c49d9bc288ac22020000000000001976a914a8be64f2249615f5abbe216764b386e9c49d9bc288ac22020000000000001976a914a8be64f2249615f5abbe216764b386e9c49d9bc288ac22020000000000001976a914a8be64f2249615f5abbe216764b386e9c49d9bc288acb8120000000000001976a914a8be64f2249615f5abbe216764b386e9c49d9bc288ac00000000

Inputs:
[

{
    "outpoint": {
        "txid": "e4a2d937b79faa46b91f49846ab5452daf1a9fb200af2b2de24f4be8890041e6",
        "outIdx": 1
    },
    "blockHeight": 840864,
    "isCoinbase": false,
    "value": 546,
    "isFinal": true,
    "token": {
        "tokenId": "e4a2d937b79faa46b91f49846ab5452daf1a9fb200af2b2de24f4be8890041e6",
        "tokenType": {
            "protocol": "SLP",
            "type": "SLP_TOKEN_TYPE_NFT1_GROUP",
            "number": 129
        },
        "amount": "100",
        "isMintBaton": false
    },
    "path": 1899
},
{
    "outpoint": {
        "txid": "a37f81222b750a581afe78211133d76f260da349c3744aafec99d8635713f434",
        "outIdx": 2
    },
    "blockHeight": 841570,
    "isCoinbase": false,
    "value": 17057,
    "isFinal": true,
    "path": 1899
}

]

Outputs:
[

{
    "value": 0,
    "script": {
        "type": "Buffer",
        "data": [
            106,
            4,
            83,
            76,
            80,
            0,
            1,
            129,
            4,
            83,
            69,
            78,
            68,
            32,
            228,
            162,
            217,
            55,
            183,
            159,
            170,
            70,
            185,
            31,
            73,
            132,
            106,
            181,
            69,
            45,
            175,
            26,
            159,
            178,
            0,
            175,
            43,
            45,
            226,
            79,
            75,
            232,
            137,
            0,
            65,
            230,
            8,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            1,
            8,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            1,
            8,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            1,
            8,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            1,
            8,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            1,
            8,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            1,
            8,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            1,
            8,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            1,
            8,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            1,
            8,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            1,
            8,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            1,
            8,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            1,
            8,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            1,
            8,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            1,
            8,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            1,
            8,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            1,
            8,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            1,
            8,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            1,
            8,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            81
        ]
    }
},
{
    "value": 546,
    "address": "ecash:qz5tue8jyjtptadthcskwe9nsm5uf8vmcgeldradu4"
},
{
    "value": 546,
    "address": "ecash:qz5tue8jyjtptadthcskwe9nsm5uf8vmcgeldradu4"
},
{
    "value": 546,
    "address": "ecash:qz5tue8jyjtptadthcskwe9nsm5uf8vmcgeldradu4"
},
{
    "value": 546,
    "address": "ecash:qz5tue8jyjtptadthcskwe9nsm5uf8vmcgeldradu4"
},
{
    "value": 546,
    "address": "ecash:qz5tue8jyjtptadthcskwe9nsm5uf8vmcgeldradu4"
},
{
    "value": 546,
    "address": "ecash:qz5tue8jyjtptadthcskwe9nsm5uf8vmcgeldradu4"
},
{
    "value": 546,
    "address": "ecash:qz5tue8jyjtptadthcskwe9nsm5uf8vmcgeldradu4"
},
{
    "value": 546,
    "address": "ecash:qz5tue8jyjtptadthcskwe9nsm5uf8vmcgeldradu4"
},
{
    "value": 546,
    "address": "ecash:qz5tue8jyjtptadthcskwe9nsm5uf8vmcgeldradu4"
},
{
    "value": 546,
    "address": "ecash:qz5tue8jyjtptadthcskwe9nsm5uf8vmcgeldradu4"
},
{
    "value": 546,
    "address": "ecash:qz5tue8jyjtptadthcskwe9nsm5uf8vmcgeldradu4"
},
{
    "value": 546,
    "address": "ecash:qz5tue8jyjtptadthcskwe9nsm5uf8vmcgeldradu4"
},
{
    "value": 546,
    "address": "ecash:qz5tue8jyjtptadthcskwe9nsm5uf8vmcgeldradu4"
},
{
    "value": 546,
    "address": "ecash:qz5tue8jyjtptadthcskwe9nsm5uf8vmcgeldradu4"
},
{
    "value": 546,
    "address": "ecash:qz5tue8jyjtptadthcskwe9nsm5uf8vmcgeldradu4"
},
{
    "value": 546,
    "address": "ecash:qz5tue8jyjtptadthcskwe9nsm5uf8vmcgeldradu4"
},
{
    "value": 546,
    "address": "ecash:qz5tue8jyjtptadthcskwe9nsm5uf8vmcgeldradu4"
},
{
    "value": 546,
    "address": "ecash:qz5tue8jyjtptadthcskwe9nsm5uf8vmcgeldradu4"
},
{
    "value": 546,
    "address": "ecash:qz5tue8jyjtptadthcskwe9nsm5uf8vmcgeldradu4"
},
{
    "value": 4792,
    "address": "ecash:qz5tue8jyjtptadthcskwe9nsm5uf8vmcgeldradu4"
}

]

patch bug in fan tx output calculation, update related tests, add tests for rendering NFT parent with NFT and NFT, get rid of dedicated component for MintNFT form and use existing one with conditions

bytesofman added inline comments.
cashtab/src/components/Etokens/MintNftForm/index.js
246 ↗(On Diff #47393)

don't think it needs to return false, that may be an artifact, potentially was required for how the antd dragger worked.

when everything is fine, it calls handleTokenIconImage and set loading to true while processing, which is expected behavior.

the complicated functions that handle the token icon upload probably should not be duplicated across CreateTokenForm and MintNftForm though...so I will instead refactor CreateTokenForm to support NFT mints.

cashtab/src/components/Home/Tx/index.js
496–501 ↗(On Diff #47393)

should be defined as a constant somewhere and not a config -- still some tech debt in cashtab to cleanup config files vs constants

but yeah should not be a magic number

bytesofman added inline comments.
cashtab/src/slpv1/index.js
605 ↗(On Diff #47405)

we had an off by one error here

when we have change, we only have 18 fan-out outputs, not 19

so, had chronik's "no burn" feature not caught this, would have been quite tricky to figure out what was going on...always burning exactly 1 parent token in every fan-out tx

remove unrelated line break change in useWallet.js

emack requested changes to this revision.Thu, Apr 25, 11:52

After fan out tx, the Mint NFT UI should indicate the total number of NFT mint inputs available. I can see this info is already available on Tx.js, so tracking it here would also be useful.

I can see the Supply decrements after each NFT mint but if I do another fan out tx to mint inputs, the supply remains unchanged.

image.png (714×610 px, 77 KB)

If child NFTs are just going to be listed per row then a collection of 100 NFTs will become very unsightly. Even if its an interim measure I'd suggest using a simple accordion or dropdown that scrolls, and then dump them all in there so the NFT UI won't drastically change based on the collection size. Mainly because this will be prod facing for a period.

image.png (817×657 px, 198 KB)

And you probably have this planned already, but just a note to have some sort of linkage back to the collection from the individual child NFT page.

On a semi related note, have you tested whether 10+ unfinalized tx spinners in Tx History cause any UI lag? My machine has too much ram so I can't tell.

image.png (919×552 px, 150 KB)

This revision now requires changes to proceed.Thu, Apr 25, 11:52

better table to present NFTs with limited size and scrollable overflow, show user number of Nft Mint Inputs available

After fan out tx, the Mint NFT UI should indicate the total number of NFT mint inputs available. I can see this info is already available on Tx.js, so tracking it here would also be useful.

Added

I can see the Supply decrements after each NFT mint but if I do another fan out tx to mint inputs, the supply remains unchanged.

This is expected behavior. Fan-out txs do not change the supply, they split a large-qty utxo into severl smaller-qty utxos. NFT mints do decrease the supply -- each NFT mint burns an NFT Mint Input.

If child NFTs are just going to be listed per row then a collection of 100 NFTs will become very unsightly.

Put some improvements in here.

  • Each NFT is its own column
  • max-height of just over 2 rows with scrollable overflow

It could always be improved but I think this is a good starting point.

image.png (411×470 px, 70 KB)

And you probably have this planned already, but just a note to have some sort of linkage back to the collection from the individual child NFT page.

We should have this but, after looking into the implementation, might be more work than it is worth. This info is only available from chronik.tx on the genesis tx of the NFT. So, it should be in the token cache. However I do not have token cache configured to capture this info, since it only applies to NFTs.

So, need some more thought on the best way to do this. Mb overkill to migrate cache. Could just call the tx from the NFT page. Also, though, the only action is probably sending NFTs, and perhaps it is better to just render this in a modal from the NFT Parent page.

On a semi related note, have you tested whether 10+ unfinalized tx spinners in Tx History cause any UI lag? My machine has too much ram so I can't tell.

Shouldn't be an issue. The animation is pure css. Each unfinalized spinner is not an individual process. When a finalized block comes in, cashtab batch processes all tx history to add finalized checkmark.

LGTM.
My only other feedback is to have a back button to navigate from the NFT back to the Collection otherwise it's a whole lot of clicks when viewing each NFT, but that can be done as part of the NFT child implementation.
Also the fan out tx seems to cost around 300-500 XEC each tx, incl fees. Should this be mentioned anywhere in case the user fans out a dozen times and is surprised by the cost. (this is in the context of a few people recently even complaining about the 5.5 XEC fee =) )

This revision is now accepted and ready to land.Thu, Apr 25, 13:31

LGTM.
My only other feedback is to have a back button to navigate from the NFT back to the Collection otherwise it's a whole lot of clicks when viewing each NFT, but that can be done as part of the NFT child implementation.
Also the fan out tx seems to cost around 300-500 XEC each tx, incl fees. Should this be mentioned anywhere in case the user fans out a dozen times and is surprised by the cost. (this is in the context of a few people recently even complaining about the 5.5 XEC fee =) )

My only other feedback is to have a back button to navigate from the NFT back to the Collection otherwise it's a whole lot of clicks when viewing each NFT

the "back" button in the browser should work for this right? one of the (few) advantages of web over mobile. tho ofc, still not really ideal UX.

Also the fan out tx seems to cost around 300-500 XEC each tx, incl fees. Should this be mentioned anywhere in case the user fans out a dozen times and is surprised by the cost. (this is in the context of a few people recently even complaining about the 5.5 XEC fee =) )

eh, we might get some complaints. but will have to see what people say. It's not unexpected -- the fan out tx has a lot of outputs, so more bytes and thus higher fee. The fee remains many orders of magnitude lower than the same operation on ETH or SOL.

This revision was automatically updated to reflect the committed changes.