Skip to content
Snippets Groups Projects
Unverified Commit abc06413 authored by ArmchairAncap's avatar ArmchairAncap
Browse files

Add xx Network asset-gating

parent 07bdf2c7
No related branches found
No related tags found
No related merge requests found
NEXTAUTH_SECRET=YOUR_SECRET_HERE
NEXTAUTH_URL=http://localhost:3000
RPC_ENDPOINT=wss://kusama-rpc.dwellir.com
\ No newline at end of file
RPC_ENDPOINT=https://xxnetwork-rpc.dwellir.com
NEXT_PUBLIC_TOKEN_ASSET_CFG={"XX":{"free":1,"id":0},"JUNK":{"standard":0.5,"premium":1,"id":5},"XRP":{"free":0,"standard":1,"premium":2,"id":7}}
- [What is it](#what-is-it)
- [Requirements](#requirements)
- [What does it do](#what-does-it-do)
- [How to run](#how-to-run)
- [What it does](#what-it-does)
- [How to run it](#how-to-run-it)
- [Use cases](#use-cases)
- [What was modified](#what-was-modified)
- [Config file and console log](#config-file-and-console-log)
- [Token and asset configuration in .env.local](#token-and-asset-configuration-in-envlocal)
- [Console log](#console-log)
- [Issues, limitations, workarounds](#issues-limitations-workarounds)
- [Address format](#address-format)
- [Asset-gating](#asset-gating)
- [Missing NEXT\_PUBLIC\_TOKEN\_ASSET\_CFG](#missing-next_public_token_asset_cfg)
- [License](#license)
- [Credits](#credits)
## What is it
......@@ -23,13 +28,13 @@ It works with static (server-rendered) and dynamic Web pages.
- Client-side: Polkadot{.js} Web3 wallet; other Web3 wallets may work
- Server-side: NodeJS with access to local or public xx chain RPC endpoint (ws[s]://)
## What does it do
## What it does
It can limit access to site or sections of site based on ownership or balance of:
It can limit access to app/site or its sections (routes) based on ownership or balance of:
- XX coins, for example more than 0, or more than 100
- assets on xx chain, for example more than 1 QTY of asset ID 5
- combination of criteria
- XX coin, for example more than 0 or more than 100
- xx Chain asset(s), for example more than 1 QTY of asset ID 5
- any combination of two or more of these criteria
Screenshot of XX coin-gating:
......@@ -41,11 +46,11 @@ With xx asset-gated code added more recently (QTY 1 of asset ID 5- see further b
![xx token- and asset-gated page](xx_polkadot_asset-gated.png)
## How to run
## How to run it
Install Polkadot{.js} extension, create xx Network wallet, fund it with some amount of xx coins (1.01, for example) and set it to work on `xx Network`.
Install Polkadot{.js} extension, create xx Network wallet, fund it with some amount of XX coin (1.01 XX, for example) and set it to work on `xx Network`.
![xx_polkadot_extension.png](./xx_polkadot_extension.png)
![xx PolkadotJS extension](./xx_polkadot_extension.png)
Download this repo, create `.env.local` in root directory, and run:
......@@ -61,19 +66,24 @@ My .env.local for non-production use:
NEXTAUTH_SECRET=123123123
NEXTAUTH_URL=http://localhost:3000
RPC_ENDPOINT=ws://192.168.1.30:63007
NEXT_PUBLIC_TOKEN_ASSET_CFG={"XX":{"free":1,"id":0},"JUNK":{"standard":0.5,"premium":1,"id":5},"XRP":{"free":0,"standard":1,"premium":2,"id":7}}
```
Go to http://localhost:3000 in the browser in which you installed Polkadot{.js} and connect the extension to http://localhost:3000, generate an address and make it restricted for use on xx Network (I hoped this would ensure balance will be looked up correctly, but it was not).
With that, I can login (see [xx_screenshot.png](./xx_screenshot.png) and balance of xx coins is correctly shown (since v1.1)).
With that, I can login (see [xx_screenshot.png](./xx_screenshot.png)) and the balance of XX coin is correctly shown (since v1.1).
(Note that, if you go to https://wallet.xx.network, Polkadot{.js} lets you add the wallet from the browser extension and show it in xx Network Wallet. Neat!)
**NOTES:**
`npm run build` builds okay, but has a fake warning about multiple versions of some package.
- you may fail to login without > 1.0 XX in your wallet. You may modify code to check for less or validate an asset balance instead
- regarding Polkadot{.js}, if you go to https://wallet.xx.network, Polkadot{.js} lets you add the wallet from the browser extension and show it in xx Network Wallet. Neat!
- `NEXT_PUBLIC_TOKEN_ASSET_CFG` doesn't play a role in checks (see below), but is required because `NEXT_PUBLIC_TOKEN_ASSET_CFG` is displayed on the home page (both table & JSON) and may be used in other pages. More on this later.
![xx Network token- and asset-gating home page](xx_polkadot_token_asset_example.png)
## Use cases
Obviously, token-gating Web sites. But, in the context of xx Network, what kinds?
Obviously, token- and asset-gating of Web sites. But, in the context of xx Network, what kinds?
- Generic infrastructure services for validators - many of whom are too cheap to pay for anything, and too lazy or privacy-conscious to register
- Access to xx Network-related services such as Haven Space directory
......@@ -90,37 +100,110 @@ So this integration is not entirely useless even for sites that can't sell xx co
## What was modified
`.env.local` and `pages/api/auth/[...nextauth].js` line 92 there's a fall back to public RPC endpoint, but I modified that one to local as well. Asset-gating was also added in that file. In the code we check the balance of asset ID 5 (which on xx Network happens to be `JUNK`) and warn if it's less than 2. I have 1 on mine, which triggers a warning like so.
- `pages/api/auth/[...nextauth].js`: modified for (XX) token and where "asset"-gating was added.
Currently, in the same file, we check for > 1 XX or more than 1 JUNK, but you can modify as you see fit:
`(accountInfo.data.free.gt(new BN(1_000_000_000)) || assetBalance >= 1)`
- `components/login.tsx`: added some functions for debugging, and logging to console, as you can see above.
- `pages/protected-api.tsx` and `pages/protected.tsx` ("static" protected page): changed the token unit to XX from KSM, changed the number of decimals to 9 from 12:
```sh
const ksmBalance = formatBalance( session.freeBalance, { decimals: 9, withSi: true, withUnit: 'XX' } )
```
- `pages/index.tsx`: layout modified for xx Network, `NEXT_PUBLIC_TOKEN_ASSET_CFG` is loaded and displayed on that page as well.
To see everything that's different compared to yk909's fork, see `git log`.
## Config file and console log
### Token and asset configuration in .env.local
`NEXT_PUBLIC_TOKEN_ASSET_CFG` contains a JSON-formatted string with a list of tokens/assets and related configuration details. In JSON, for easier viewing:
```json
{
"XX": {
"free": 1,
"id": 0
},
"JUNK": {
"standard": 0.5,
"premium": 1,
"id": 5
},
"XRP": {
"free": 0,
"standard": 1,
"premium": 2,
"id": 7
}
}
```
The first entry is the native currency of xx Network, XX coin, for which the ID is meaningless and I use 0 as xx Network assets can't have that ID.
There's just one tier - free - and you need more than 1 XX to get in even for that (i.e. it's free, but not open access).
The second item is the JUNK asset; that ID is real (it's 5, on xx Chain) and we have two service tiers: standard (0.5 JUNK required) and premium (1 JUNK required). (Just FYI, JUNK is not a divisible asset so you can't have 0.5; anyone who has any will have 1 or more. Just make sure you compare like vs. like in JS.)
Note that this JSON is currently **NOT used** in actual gating checks which are two, as I've mentioned earlier. To save you time, we check for > 1 XX or > 1 JUNK:
`(accountInfo.data.free.gt(new BN(1_000_000_000)) || assetBalance >= 1)`
If XX and one "hard-coded" asset ID is enough, just change the assetId and minimum amount in `[...nextauth].js` and modify logic according to your requirements. This is **the default**.
While that may work for many single-app sites, [issue #2](https://github.com/armchairancap/polkadot-js-tokengated-website/issues/2) is meant to help some more complex use cases, so this JSON config has been added.
If you rely on `NEXT_PUBLIC_TOKEN_ASSET_CFG` you will need to change code logic to loop through the list of XX and asset(s) and for assets look up the balance of each. I don't know what people may want to do with this and how (combination of several assets, one asset per route...), so I've just made examples of looping and checking against all assets. But for now the checks remain as mentioned above - i.e. JSON values are ignored.
Let's see how that's done by looking at the console log.
## Console log
There's (excessive) logging to console that I didn't clean up in order to make it easier to debug until you get it right.
After you allow the app to connect to an address, and login, `[...nextauth].js` will enforce xx Network token (XX) address and query XX balance first.
```raw
2024-11-11 15:47:59 API/INIT: MetadataApi not available, rpc::state::get_metadata will be used.
ksmAddress: 6aCE19CakDJBp8wnVHB2HpHYfaeNiwx2RxQcsAcyWvPLVn5k
Wallet address on xx Network: 6aCE19CakDJBp8wnVHB2HpHYfaeNiwx2RxQcsAcyWvPLVn5k
Wallet balance on xx Network: 1840527453
```
That's 1.84 XX. Then it will read the JSON config file, check it against assetID hard-coded in the file and show asset name (from JSON) and ID, as well as check the balance of that asset (which is 1 JUNK). Then we check for > 1 XX and >= 1 JUNK and warn if there's less than 2 JUNK.
```raw
Token-asset config in [...nextauth].ts: Map(3) {
'XX' => { free: 1, id: 0 },
'JUNK' => { standard: 0.5, premium: 1, id: 5 },
'XRP' => { free: 0, standard: 1, premium: 2, id: 7 }
}
Token-asset config entry: JUNK id 5
Token-asset config entry found: JUNK id 5
Account balance for asset 5 and address 6aCE19CakDJBp8wnVHB2HpHYfaeNiwx2RxQcsAcyWvPLVn5k: 1
Asset balance: 1
Warning: Account balance for asset 5 is less than 2.
Asset balance going into returned object: 1
```
This below is already the auth stuff. This address format shown here is Polkadot but that doesn't matter (see further below) as our checks are already done against xx Chain.
```json
token {
name: 'the-dude',
sub: '5GxeeFALkRvjnNgkiMjiP6q2GGnZ1ZmFyjCusHG4VoezqZSN',
freeBalance: '0x0000000000000000000000006db4385d',
iat: 1730995852,
exp: 1733587852,
jti: 'c7a8e87a-0bff-46da-a5be-ffc51726d6dc'
assetBalance: '1',
iat: 1731311279,
exp: 1733903279,
jti: '1116f94b-ad0b-41bb-b2dc-730c7023e928'
}
```
Currently, in the same file, we check for > 1 XX or more than 1 JUNK, but you can modify as you see fit:
`(accountInfo.data.free.gt(new BN(1_000_000_000)) || assetBalance >= 1)`
In `components/login.tsx` I added some functions for debugging, and logging to console, as you can see above.
In `pages/protected-api.tsx` and `pages/protected.tsx` ("static" protected page), changed KSM to XX and changed the number of decimals to 9 from 12:
```sh
const ksmBalance = formatBalance( session.freeBalance, { decimals: 9, withSi: true, withUnit: 'XX' } )
```
To see everything that's different compared to yk909's fork, see git log.
## Issues, limitations, workarounds
### Address format
......@@ -136,7 +219,7 @@ token {
exp: 1732548938,
jti: '3252212b-0fb2-4a82-88d0-3ceaf5886aa1'
}
```
```
The FAQs say [it's normal to see another address](https://polkadot.js.org/docs/keyring/FAQ#my-pair-address-does-not-match-with-my-chain).
......@@ -144,7 +227,21 @@ You can configure the wallet to default to xx Network, by the way (see the scree
### Asset-gating
This may be xx Network-specific and may not work with other Substrate-based chains. You need to check if your chain supports Assets, and how. It seems Polkadot removed their Assets pallet, for example.
This may be xx Network-specific and may not work with other Substrate-based chains. You need to check if your chain supports Assets, and how. It seems Polkadot removed their Assets pallet and has them on a parachain, it appears.
### Missing NEXT_PUBLIC_TOKEN_ASSET_CFG
As mentioned several times, this variable is shown and output in logs, as well as compared to the hard-coded assetID in the app, but gating doesn't use that file.
If you don't have at least some items (XX and and an asset), code may not work as expected because the variable will be an empty JSON file (`{}`) which may cause errors.
Since the existence of that variable doesn't impact token or asset checks, you may as well leave it. Or remove it if you wish, as long as you can fix any errors that causes.
## License
The early contributors didn't attach any, so you may want to check with them or assume.
Whatever code and documentation I've added, all that is The 2-Clause BSD License.
## Credits
......
......@@ -31,6 +31,19 @@ declare module 'next-auth' {
}
}
type TokenLevels = {
free?: number;
standard?: number;
premium?: number;
id?: number;
};
export const tokenAssetConfig = {
// load JSON file with token asset configuration from TOKEN_ASSET_CFG variable: {"JUNK":{"standard": 1,"premium":2},"GOLD":{"free": 0, "standard": 1, "premium":2}}
TAConfig: new Map<string, TokenLevels>(Object.entries(JSON.parse(process.env.NEXT_PUBLIC_TOKEN_ASSET_CFG || '{}')))
};
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
......@@ -95,7 +108,7 @@ export const authOptions: NextAuthOptions = {
}
// verify the account has the defined token
const wsProvider = new WsProvider(process.env.RPC_ENDPOINT ?? 'ws://192.168.1.3:63007');
const wsProvider = new WsProvider(process.env.RPC_ENDPOINT ?? 'https://xxnetwork-rpc.dwellir.com');
const api = await ApiPromise.create({ provider: wsProvider });
await api.isReady;
......@@ -112,8 +125,20 @@ export const authOptions: NextAuthOptions = {
// AA: asset balance check
const assetId = 5; // AA: Replace with your asset ID
const accountAssetInfo = await api.query.assets.account(assetId, ksmAddress);
// initialize assetBalance as 0
// AA: initialize assetBalance as 0
let assetBalance = 0;
// AA: get Token-Asset Config
console.log('Token-asset config in [...nextauth].ts: ', tokenAssetConfig.TAConfig);
// AA: find nested 'id' in tokenAssetConfig.TAConfig.entries() and compare with assetID value
for (const [token, levels] of tokenAssetConfig.TAConfig.entries()) {
for (const [level, value] of Object.entries(levels)) {
if (value === assetId) {
console.log('Token-asset config entry: ', token, level, value);
console.log('Token-asset config entry found: ', token, level, value);
}
}
}
if (accountAssetInfo.isEmpty) {
console.log(
`No balance found for asset ${assetId} and address ${ksmAddress}`
......@@ -131,7 +156,8 @@ export const authOptions: NextAuthOptions = {
};
// end asset balance check
console.log('Asset balance going into returned objected: ', assetBalance.toString());
console.log('Asset balance going into returned object: ', assetBalance.toString());
if (accountInfo.data.free.gt(new BN(1_000_000_000)) || assetBalance >= 1) {
// if the user has a free balance > 1 XX or JUNK !> 0, we let them in
return {
......
......@@ -8,14 +8,31 @@ import Link from 'next/link';
const inter = Inter({ subsets: ['latin'] });
type TokenLevels = {
free?: number;
standard?: number;
premium?: number;
id?: number;
};
export const tokenAssetConfig = {
// load JSON file with token asset configuration from TOKEN_ASSET_CFG variable: {"JUNK":{"standard": 1,"premium":2},"GOLD":{"free": 0, "standard": 1, "premium":2}}
TAConfig: new Map<string, TokenLevels>(Object.entries(JSON.parse(process.env.NEXT_PUBLIC_TOKEN_ASSET_CFG || '{}')))
};
console.log("Configuration JSON in index.tsx: ", tokenAssetConfig.TAConfig);
export default function Home() {
const preRenderedTokenAssetCfg = JSON.stringify(Object.fromEntries(tokenAssetConfig.TAConfig), null, 2).replace(/ /g, '&nbsp;').replace(/\n/g, '<br/>');
console.log("Configuration pre-rendered HTML: ", preRenderedTokenAssetCfg);
return (
<>
<Head>
<title>Polkadot Tokengated Tutorial</title>
<title>Polkadot Token- and Asset-gated Tutorial</title>
<meta
name="description"
content="Demo Tutorial dApp using polkadot js api and next auth to build a tokengated website"
content="Demo Tutorial dApp using Polkadot.js API (xx Network flavor) and next-auth to build a token-gated and asset-gated web application."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
......@@ -27,19 +44,63 @@ export default function Home() {
<div className={styles.center}>
<Image
className={styles.logo}
src="/polkadot.svg"
alt="Polkadot Logo"
src="/xxNetwork.svg"
alt="XX Network Logo"
width={240}
height={77}
priority
/>
<p className={inter.className}>Tokengated Tutorial Demo</p>
<p className={inter.className}>Token- and asset-gated Tutorial Demo for xx Network (Substrate-based token & assets)</p>
</div>
<LoginButton />
<h3>Membership Levels</h3>
<p>&nbsp;</p>
<p className={inter.className}>This site provides different levels of service based on the holdings of the wallet you choose to login with.</p>
<p>&nbsp;</p>
<table className={styles.tokenAssetTable} style={{ width: '30%' }}>
<thead>
<tr>
<th style={{ textAlign: 'left', margin: '2px' }}>Token</th>
<th style={{ textAlign: 'left', margin: '2px' }}>Level</th>
<th style={{ textAlign: 'center', margin: '2px' }}>Balance</th>
</tr>
</thead>
<tbody>
{Array.from(tokenAssetConfig.TAConfig.entries()).map(([token, levels]) =>
Object.entries(levels).map(([level, value]) => (
level !== 'id' && (
<tr key={`${token}-${level}`}>
<td style={{ margin: '2px' }}>{token}</td>
<td style={{ margin: '2px' }}>{level}</td>
<td style={{ textAlign: 'center', width: 20, margin: '2px' }}>{value}</td>
</tr>
)
))
)}
</tbody>
</table>
<p>&nbsp;</p>
<h3>Token-Asset Configuration</h3>
<p>&nbsp;</p>
<p>.env.local.NEXT_PUBLIC_TOKEN_ASSET_CFG (JSON):</p>
<p>&nbsp;</p>
<div className={styles.tokenAssetCfg} style={{ backgroundColor: '#0DB9CB', width: '20%' }} dangerouslySetInnerHTML={{ __html: preRenderedTokenAssetCfg }} />
<p>&nbsp;</p>
<div className={styles.description}>
<a href="https://github.com/niklasp/polkadot-js-tokengated-webssite" target="_blank">
<Image
<Link href="/">
🏠 Home page
</Link>
<Link href="/protected" rel="noopener noreferrer">
🔐 To /protected (SSR)
</Link>
<Link href="/protected-api" rel="noopener noreferrer">
🔐 To /protected-api (Static)
</Link>
<Link href="https://polkadot.study/tutorials/tokengated-polkadot-next-js/intro">
🎓 Tutorial
</Link>
<a href="https://github.com/armchairancap/polkadot-js-tokengated-website" target="_blank">
<Image
src="/github.svg"
alt="Github Repository"
className={styles.githubLogo}
......@@ -47,17 +108,8 @@ export default function Home() {
height={16}
priority
/>
View the repo
</a>
<Link href="https://polkadot.study/tutorials/tokengated-polkadot-next-js/intro">
🎓 View the Tutorial
</Link>
<Link href="/protected" rel="noopener noreferrer">
🔐 Go to /protected (SSR)
</Link>
<Link href="/protected-api" rel="noopener noreferrer">
🔐 Go to /protected-api (Static)
</Link>
Source code
</a>
</div>
</main>
</>
......
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 25.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 525 175" style="enable-background:new 0 0 525 175;" xml:space="preserve"><script xmlns=""/>
<style type="text/css">
.st0{fill:#0DB9CB;}
.st1{enable-background:new ;}
.st2{fill:#3D3D3D;}
</style>
<g>
<g>
<path class="st0" d="M123.8,72.8c1.6,3.6,2.6,7.6,2.6,11.7c0,0.1,0,0.3,0,0.4L140,72.8V58.4L123.8,72.8z"/>
<path class="st0" d="M122.1,84.6c0-14.4-12.9-26.1-28.7-26.1v10.8c9.7,0,17.6,6.6,17.9,14.8l-7.3,6.5c1.9,3.4,3.2,7.2,3.7,11.2 l6.5-5.8c4.6,8.8,14.5,14.8,25.9,14.8V100C130.1,99.9,122.1,93,122.1,84.6z"/>
<path class="st0" d="M121.4,130.8V120c-9.9,0-17.9-6.9-17.9-15.3c0-14.4-12.9-26.1-28.7-26.1v10.8c8.4,0,15.5,5,17.4,11.7 l-3.8,3.4l-7.8,7l0,0l-5.8,5.1V131l19.6-17.4C98.3,123.5,108.9,130.8,121.4,130.8z"/>
</g>
<g class="st1">
<path class="st2" d="M190.7,89.1v21.6h-8.8V90.3c0-6.7-3.3-9.9-9-9.9c-6.3,0-10.5,3.8-10.5,11.3v19h-8.8V73h8.4v4.9 c2.9-3.5,7.5-5.3,13-5.3C184,72.6,190.7,77.7,190.7,89.1z"/>
</g>
<g class="st1">
<path class="st2" d="M234.7,94.7h-29.5c1.1,5.5,5.6,9,12.2,9c4.2,0,7.5-1.3,10.2-4.1l4.7,5.4c-3.4,4-8.7,6.1-15.2,6.1 c-12.6,0-20.8-8.1-20.8-19.3s8.2-19.2,19.5-19.2c11,0,19,7.7,19,19.5C234.9,92.8,234.8,93.8,234.7,94.7z M205.1,88.8h21.4 c-0.7-5.4-4.9-9.2-10.6-9.2C210.1,79.7,206,83.3,205.1,88.8z"/>
</g>
<g class="st1">
<path class="st2" d="M265.9,108.6c-2.1,1.7-5.3,2.5-8.5,2.5c-8.2,0-13-4.4-13-12.7v-18h-6.2v-7h6.2v-8.6h8.8v8.6h10.1v7h-10.1 v17.9c0,3.7,1.8,5.6,5.2,5.6c1.8,0,3.6-0.5,4.9-1.6L265.9,108.6z"/>
</g>
<g class="st1">
<path class="st2" d="M334,73l-13.9,37.6h-8.5l-9.7-25.9l-9.9,25.9h-8.5L269.7,73h8.3l9.9,27.8L298.3,73h7.4l10.2,27.9L326.1,73 H334z"/>
</g>
<g class="st1">
<path class="st2" d="M335.9,91.8c0-11.3,8.5-19.2,20-19.2c11.7,0,20.1,8,20.1,19.2s-8.4,19.3-20.1,19.3 C344.4,111.2,335.9,103.1,335.9,91.8z M367.2,91.8c0-7.2-4.8-11.8-11.2-11.8c-6.3,0-11.1,4.6-11.1,11.8s4.8,11.8,11.1,11.8 C362.4,103.6,367.2,99,367.2,91.8z"/>
</g>
<g class="st1">
<path class="st2" d="M404.9,72.6V81c-0.8-0.1-1.4-0.2-2-0.2c-6.7,0-10.9,3.9-10.9,11.6v18.3h-8.8V73h8.4v5.5 C394,74.6,398.6,72.6,404.9,72.6z"/>
</g>
<g class="st1">
<path class="st2" d="M428,94.5l-6.6,6.2v9.9h-8.8V58.4h8.8V90l18.3-16.9h10.6l-15.7,15.8l17.2,21.9h-10.7L428,94.5z"/>
</g>
</g>
</svg>
\ No newline at end of file
......@@ -98,7 +98,7 @@
justify-content: center;
align-items: center;
position: relative;
padding: 4rem 0;
padding: 2rem 0;
flex-direction: column;
font-family: var(--font-mono);
}
......
......@@ -9,11 +9,11 @@
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
--polkadot-color: #e6007a;
--polkadot-rgb: 230, 0, 122;
--polkadot-purple: #8e24aa;
--polkadot-color: #0db9cb;
--polkadot-rgb: 13, 185, 203;
--polkadot-purple: #1976d2;
--primary-glow: radial-gradient(#e6007aaa, rgba(1, 65, 255, 0));
--primary-glow: radial-gradient(#ffffff, rgba(240, 240, 255, 0));
--secondary-glow: linear-gradient(
to bottom right,
rgba(1, 65, 255, 0),
......@@ -70,4 +70,4 @@ a {
a:hover {
text-decoration: none;
}
\ No newline at end of file
}
xx_polkadot_token_asset_example.png

124 KiB

0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment