Skip to main content

NFT-gated access on xx Network

· 9 min read

Tokens, assets, NFTs, WTF

xx Network has its native currency XX coin, assets and NFTs. Unfortunately, XX is referred to as a "token" (not a currency).

Most of the Polkadot SDK-based chains promote NFTs, but xx Network hasn't adopted those - at least not in the official wallet.

But we can issue and use assets, which is a sensitive term (the SEC, etc.) even when used in non-asset contexts like this one.

To me XX coin is a currency, and "assets" are tokens, but this confusing official terminology is something we have to deal with.

Token-gated access control

note

This really means currency-gated (with XX coins).

I modified a Kusama example for xx Network (xx chain) and made it available here.

It checks the balance on the address in your Polkadot.js wallet and depending on that value it allows you to access the site.

Assets in xx Network wallet

That's great, so all Substrate-based chains can do this and limit access to apps to hodlers or whales, etc. You don't have 100xx in your wallet? Piss off!

But what about them "soulbound tokens"?

That's a different lookup/API, for "assets" rather than "token" balances (for XX).

Asset-gated access control

note

This really means token-gated (with "assets").

We can also limit access to hodlers of certain assets, and by that I mean token-like assets available on xx Network.

In the case you've been so engaged that you missed their existence, you can find them in xx Network Wallet under Network > Assets.

Let's consider two, test (ID: 4) and Intelligence (ID: 42).

We need to know how to query their balance in user's wallet.

For the impatient JS users - find sample JS code in the same repository.

Query (check asset balances)

It's unbelievable and ludicrous, but it took me many hours to find this info. As if no one needs this stuff.

I find that hard to believe! But look above - after several years of mainnet and there's just half a dozen test-grade assets, so maybe that's true?

Anyway, let's go:

from substrateinterface import SubstrateInterface

substrate = SubstrateInterface(
url="ws://192.168.1.30:63007"
)

# Asset test
ASSET_ID = 4 # asset ID: test
ACCOUNT = '6Xo3ESdU9nW3iiAk5ZqDA7kKqYJMcJFGiRz91LkoZKvvTjAV' # owner / issuer
account_info = substrate.query(
module='Assets',
storage_function='Account',
params=[ASSET_ID, ACCOUNT],
)

print(f'Balance: {account_info["balance"]}')
# Balance: 100000000000

# Asset Intelligence
ASSET_ID = 42 # asset ID: Intelligence
ACCOUNT = '6ZsMetbm4yoj9JQJdmhn2dKScFNkUnxULXwTBsNpmL1FM3KY' # owner / issuer

account_info = substrate.query(
module='Assets',
storage_function='Account',
params=[ASSET_ID, ACCOUNT],
)

print(f'Balance: {account_info["balance"]}')
# Balance: 42

All right!!! I think this should work for other addresses (not just the owner) but I don't know if any of these owners have sent them to other people.

Hey, that issuance amount of test looks weird - how come there's 100000000000 of 'em when the screenshot says 100?

The reason is assets may have different properties. We can use TFM (where is it?) or try the "create asset" workflow to see what goes in.

xx Network asset create options

So, it appears Intelligence is an integer-based asset, while test has a crapload of decimals.

That's good to know, but it doesn't matter much to us in this particular workflow: we don't need to know how to look up an asset's properties as we usually accept one or more known assets. If you're interested in that, however, find it here - it's api.query.assets.metadata (in JavaScript). Polkadot.js API details related to assets can be found in the repo.

In JS I managed to do this:

import { ApiPromise, WsProvider } from "@polkadot/api";
import type { StorageKey, u32 } from '@polkadot/types';
const assetDetails = await api.query.assets.asset(assetId);
console.table(assetDetails.toHuman());
const assetMetadata = await api.query.assets.metadata(assetId);
console.table(assetMetadata.toHuman());

.. and get this. But these are the details I don't care about much, as I've mentioned.

┌──────────────┬────────────────────────────────────────────────────┐
│ (index) │ Values │
├──────────────┼────────────────────────────────────────────────────┤
│ owner │ '6VXfntvzZTzY2YPA4KtSVZMx1YqPFTo7o8qtddNLB4K91Mcn' │
│ issuer │ '6VXfntvzZTzY2YPA4KtSVZMx1YqPFTo7o8qtddNLB4K91Mcn' │
│ admin │ '6VXfntvzZTzY2YPA4KtSVZMx1YqPFTo7o8qtddNLB4K91Mcn' │
│ freezer │ '6VXfntvzZTzY2YPA4KtSVZMx1YqPFTo7o8qtddNLB4K91Mcn' │
│ supply │ '102' │
│ deposit │ '100,000,000,000' │
│ minBalance │ '1' │
│ isSufficient │ false │
│ accounts │ '3' │
│ sufficients │ '0' │
│ approvals │ '0' │
│ status │ 'Live' │
└──────────────┴────────────────────────────────────────────────────┘
┌──────────┬──────────────────┐
│ (index) │ Values │
├──────────┼──────────────────┤
│ deposit │ '18,000,000,000' │
│ name │ 'JUNK' │
│ symbol │ 'JUNK' │
│ decimals │ '0' │
│ isFrozen │ false │
└──────────┴──────────────────┘

I got lucky with api.query.assets.account(assetId, accountAddress) hours later.

I used the issuing address to mint 1 JUNK to a beneficiary address.

assetId = 5 # Asset: JUNK
accountAddress = '6aCE19CakDJBp8wnVHB2HpHYfaeNiwx2RxQcsAcyWvPLVn5k'
const accountInfo = await api.query.assets.account(assetId, accountAddress);
if (accountInfo.isEmpty) {
console.log(
`No balance found for asset ${assetId} and address ${accountAddress}`
);
} else {
console.log(
`Account balance for asset ${assetId} and address ${accountAddress}:`,
accountInfo.toHuman()
);
}

Log:

Account balance for asset 5 and address 6aCE19CakDJBp8wnVHB2HpHYfaeNiwx2RxQcsAcyWvPLVn5k:
{ balance: '1', isFrozen: false, reason: 'Consumer', extra: null }

My Github repo contains a working code example. It's poorly written but it works and you may be able to improve on it.

The Wallet has some smart examples in these files, but they're too smart for me - I could't figure out how to use them.

  • packages/page-assets/src/Balances/useBalances.ts
  • packages/page-assets/src/useAssetIds.ts
  • packages/page-assets/src/useAssetInfos.ts

Buying and selling assets on xx chain

Houston, we have a problem... I'm not sure how to do that.

OTC to the rescue! Create and publish Haven Space address for your OTC store and sell assets there. It sucks, but it can be automated. How?

You can build a bot - I once created a crude one - that reads comments in your channel and sells this stuff.

Price list for your Validator Monthly Intelligence newsletter:

  • Basic subscription - 10 XX/mo
  • Premium - 100 XX/mo

Buyer says "I've transferred 10 XX (extrinsic hash attached), please credit me 1 Intelligence to this address 6ZLG..":

/buy 1 Intelligence 6VwDT9VMgPrswqtJtqUH4PU7DkcaHggJjyjnkuUZyNA5c5j1 \
0xaba65810f33f905e2d136e6a12c0a7e4ae7111a4f6c59415ffa83a3d0d7663c0

The bot checks that transaction (everything checks out, 10XX received). It mints 1 Intelligence to 6ZLGrh6Mu7uDioL7bAVS4kTsRSez7w1Lz18fiApUqRHNWnxd and appends an extrinsic hash and timestamp for easier troubleshooting.

/mint 1 6VwDT9VMgPrswqtJtqUH4PU7DkcaHggJjyjnkuUZyNA5c5j1 \
0xaba65810f33f905e2d136e6a12c0a7e4ae7111a4f6c59415ffa83a3d0d7663c0 {timestamp}

If something fails, Haven keeps data for 21 days so there's enough time to chit-chat in another Space (for human-powered support) where refunds and other issues can be dealt with.

I'm sure there are some edge cases I haven't thought of, but there's no reason why this can't work and there must be some detailed production-worthy examples in other ecosystems.

Anyway, let's move on. As Teddy KGB said, "Pay him... Pay that man his fxxxing money". Or send the man his freaking asset:

from substrateinterface import SubstrateInterface

KEYPAIR = '....'

# Asset: Intelligence
ASSET_ID = 5
ASSET_QTY = 1
# Subscriber's wallet to use in Polkadot{.js} for authorization
SUBSCRIBER_ID = '6VwDT9VMgPrswqtJtqUH4PU7DkcaHggJjyjnkuUZyNA5c5j1'

call = substrate.compose_call(
call_module="Assets",
call_function="transfer",
call_params={'id': ASSET_ID, 'target': SUBSCRIBER_ID, 'amount': ASSET_QTY}
)

extrinsic = substrate.create_signed_extrinsic(call=call, keypair=KEYPAIR, era={'period': 64})
result = substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True)

I haven't tried this one yet, by the way.

I'm not sure about the difference between minting and transferring - obviously the former increments the issuance while the latter sends existing, but I haven't looked at the minting API in Python yet. I wonder if minting is a term for asset transfer (I hope not, as it implies additional issuance).

Dealing with subscription expiration

Yes, there are more problems...

Next month that guy will still have his 1 Intelligence. But we need to charge him again. What can we do?

Normally we'd use "Smart Contracts", but we can't (or at least I don't know how to do that on xx chain), so we have to come up with workarounds such as:

  • Check for a new token (Intelligence-Jan, Intelligence-Feb) every month (you can pre-release, and not sell, 12 of them at once), or
  • Sell 12 month subscriptions and issue a new asset every year , or
  • Buy back tokens at a tiny fraction of the original price, or
  • Check date of last pay the wallet did as well as the balance (complicated)
note

I wonder if 21 day subscription periods would be better than customary 30 day periods, because Haven retains server-side chat data that long. If bots work well, 30 days or longer shouldn't be a problem.

Use cases

Few wallets have good reputation, but there are folks out there who could probably charge for monthly subscription.

  • Newsletter (say, monthly validator analysis for stakers/nominators)
  • xx chain-related reports (nominator pays 10xx for staking payouts and Web reports/stats, for example)
  • Discord bots for asset-gated access to special channels
  • Private training/consulting (for Haven admins and users, e.g. 1 unit of an asset buys you 1 minute)
  • Classic "soulbound" assets given away for free (no OTC trading necessary)

Managed Haven instance hosting sounds like another possibility, but that idea sucks:

  • It requires absolute trust
  • Users who access such instances immediately identify themselves by their wallet ID (and xx is not a privacy coin) and maybe IP address, browser fingerprint and more

Alien assets and NFTs

Polkadot has a rich asset/NFT ecosystem that can do more and even has bridges to other L1 chains (alien NFTs, one might say).

While that's nice, it's more than we need here.

Of course, xx chain could adopt some of those features, but I think it objectively doesn't need them.

Summary

In xx Network wallet NFTs are there (based on the xxchain code pallet-nfts v4.0.0 is used), but there's no way to issue them from the wallet. You can find the NFT Pallet documentation here, just mind the fact that xx Network Wallet won't let owners see them unless wallet itself gets some upgrades.

As we've seen assets exist, but aren't likely to be used as "assets".

xx Network Wallet with assets and NFTs

The xx Network community hasn't done much to monetize Web (or should I say "Web3" to highlight my "DeFi" prowess) access.

One of the reasons is surely lack of examples and documentation, and another is a small market size.

With XX coin-gated and NFT-gated apps (based on xx "assets"), that is easy to do now. Get the code here.

Market for xx chain-related token- and NFT-gated services (based on xx chain "assets") is still small, but it does exist. Anyone who builds something and makes it available for others, lowers the entry barrier for more xx Network services. Let us hope such services emerge in coming months.