diff --git a/.github/workflows/release-gear-idea.yml b/.github/workflows/release-gear-idea.yml index b5b711754..cc7ab4c38 100644 --- a/.github/workflows/release-gear-idea.yml +++ b/.github/workflows/release-gear-idea.yml @@ -1,144 +1,70 @@ -name: 'Gear Idea Release' - -on: - push: - branches: ['main', 'stable'] - paths: - - idea/gear/meta-storage/package.json - - idea/gear/faucet/package.json - - idea/gear/frontend/package.json - - idea/gear/squid/package.json - - idea/gear/explorer/package.json - workflow_dispatch: - inputs: - service: - type: choice - description: Service to release - options: - - frontend - - faucet - - squid - - meta-storage - - explorer - - all - - backend - required: true - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.AWS_REGION }} - KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }} - -jobs: - check-version: - runs-on: ubuntu-latest - outputs: - skip: ${{ steps.set_skip.outputs.skip }} - permissions: - contents: read - packages: write - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - fetch-depth: 2 - - - name: Set skip=false on workflow_dispatch - if: ${{ github.event_name == 'workflow_dispatch' }} - run: echo "skip=false" >> $GITHUB_OUTPUT - - - name: Get current version - if: ${{ github.event_name != 'workflow_dispatch' }} - id: cur_version - run: | - version=$(jq -r .version idea/gear/common/package.json) - echo "Current version: $version" - echo "version=$version" >> $GITHUB_OUTPUT - - - name: Get previous version - if: ${{ github.event_name != 'workflow_dispatch' }} - id: prev_version - run: | - version=$(git show HEAD~1:idea/gear/common/package.json | jq -r .version) - echo "Previous version: $version" - echo "version=$version" >> $GITHUB_OUTPUT - - - name: Compare versions and set SKIP env - if: ${{ github.event_name != 'workflow_dispatch' }} - id: set_skip - run: | - if [[ "${{ steps.cur_version.outputs.version }}" != "${{ steps.prev_version.outputs.version }}" ]]; then - echo "skip=false" >> $GITHUB_OUTPUT - else - echo "skip=true" >> $GITHUB_OUTPUT - fi - - set-env-tag: - runs-on: ubuntu-latest - outputs: - environment: ${{ steps.get_env.outputs.environment }} - needs: [check-version] - if: ${{ needs.check-version.outputs.skip != 'true' }} - permissions: - contents: read - packages: write - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Get environment - id: get_env - run: | - if [ "${{ github.ref }}" == "refs/heads/stable" ]; then - echo "environment=production" >> $GITHUB_OUTPUT - else - echo "environment=staging" >> $GITHUB_OUTPUT - fi - - build-frontend-image: - runs-on: ubuntu-latest - needs: [set-env-tag] - environment: ${{ needs.set-env-tag.outputs.environment }} - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Check if should be built - id: check - uses: './.github/actions/should-be-built' - with: - patterns: 'frontend,all' - input: ${{ github.event.inputs.service }} - event_name: ${{ github.event_name }} - - - name: Log in to the github container registry - if: ${{ steps.check.outputs.ok == 'true' }} - uses: docker/login-action@master - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push "gear-idea-frontend" Docker image - if: ${{ steps.check.outputs.ok == 'true' }} - uses: docker/build-push-action@master - with: - file: idea/gear/frontend/Dockerfile +分支:取消进行中: true: 真的: 真的克'非 vft'['非 vft']注册表:ghcr.io + +取消进行中: true +推: +分支:['非 vft']['非 vft']注册表:ghcr.io: 克]: ghcr.io工作流程_调度:] +取消进行中: true工作流程_调度:真的: 真的 +输入: +服务: +类型:选择 +描述:服务发布 +选项: +- 前端前端前端前端 +必填:真实 + +并发: +AWS_ACCESS_KEY_ID 团体${{github.workflow}}-${{github.ref}}{{github.workflow}}-${{github.ref}} +取消进行中: true: 真的: 真的 + +环境:::: +注册表:ghcr.io: 克hcr.io: 克hcr.io: 克: ghcr.io +IMAGE_NAME:${{ github.repository }}: ${{github.repository}}: ${{github.repository}} +AWS_ACCESS_KEY_ID:${{secrets.AWS_ACCESS_KEY_ID }}: ${{秘密.AWS_ACCESS_KEY_ID}}: ${{秘密.AWS_ACCESS_KEY_ID}} +AWS_SECRET_ACCESS_KEY:${{秘密.AWS_SECRET_ACCESS_KEY}}: ${{秘密.AWS_SECRET_ACCESS_KEY}} +AWS_REGION: ${{秘密.AWS_REGION}}: ${{秘密.AWS_REGION}}行动/结帐@v6姓名: 等: ${{秘密.KUBE_CONFIG_DATA}}{{秘密.AWS_REGION}} +KUBE_CONFIG_DATA: ${{秘密.KUBE_CONFIG_DATA}}: ${{秘密.KUBE_CONFIG_DATA}}: ${{秘密.KUBE_CONFIG_DATA}}: ${{秘密.KUBE_CONFIG_DATA}}职位::${{秘密.KUBE_CONFIG_DATA}}: ${{秘密.KUBE_CONFIG_DATA}} + +职位::: +设置环境标签::行动/结帐@v6姓名: 等: 等环境 +连续运行: 在: 在buntu-最新版: 在: 在buntu-最新版: 在: ubuntu-最新版 +输出::: +环境: ${{步骤.get_env.outputs.environment}}: ${{步骤.get_env.outputs.environment}}: ${{步骤.get_env.outputs.environment}}: ${{步骤.get_env.outputs.environment}} +步骤::: +- 姓名: 签: 签出存储库姓名: 签: 签出存储库 +用途: 姓: 姓: 姓: 姓名 + +行动/结帐@v6姓名: 等: 等: 等: 等环境 +ID: 克等环境: 克等环境 +跑步: |: | +echo“环境=生产”>> $GITHUB_OUTPUT + +构建前端图像构建前端图像 +连续运行: ubuntu-最新版: ubuntu-最新版 +需要: [设置环境标签]: [设置环境标签] +环境: ${{need.set-env-tag.outputs.environment}}: ${{need.set-env-tag.outputs.environment}} +权限:: +内容: r电子头: r电子头 +包: 在喜欢: 在喜欢 + +步骤:: +- 姓名: 签出存储库姓名: 签出存储库 +用途:: 一个动作/结账@v6 + +- 姓名用途用途 +用途: 和 +和: +注册表: ${{环境注册表}} +用户名: ${{github.actor}} +密码: ${{秘密.GITHUB_TOKEN}} + + - 日本名字和姓氏 +用途“齿轮创意前端”docker/build-push-action@master +和: +名称:“齿轮创意发布”: idea/gear/frontend/Dockerfile push: true - tags: | - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-gear-idea-frontend:${{ needs.set-env-tag.outputs.environment }} - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-gear-idea-frontend:${{ needs.set-env-tag.outputs.environment }}-${{ github.sha }} + 标签: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-gear-idea-frontend:temp + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-gear-idea-frontend:temp-${{ github.sha }} build-args: | VITE_NODE_ADDRESS=${{ secrets.REACT_APP_NODE_ADDRESS }} VITE_DEFAULT_TRANSFER_BALANCE_VALUE=${{ secrets.REACT_APP_DEFAULT_TRANSFER_BALANCE_VALUE }} @@ -153,168 +79,8 @@ jobs: VITE_MAINNET_DNS_API_URL=${{ secrets.VITE_MAINNET_DNS_API_URL }} VITE_CODE_VERIFIER_API_URL=${{ secrets.VITE_CODE_VERIFIER_API_URL }} - build-faucet-image: - runs-on: ubuntu-latest - needs: [set-env-tag] - environment: ${{ needs.set-env-tag.outputs.environment }} - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Check if should be built - id: check - uses: './.github/actions/should-be-built' - with: - patterns: 'faucet,all,backend' - input: ${{ github.event.inputs.service }} - event_name: ${{ github.event_name }} - - - name: Log in to the github container registry - if: ${{ steps.check.outputs.ok == 'true' }} - uses: docker/login-action@master - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push "gear-idea-faucet" Docker image - if: ${{ steps.check.outputs.ok == 'true' }} - uses: docker/build-push-action@master - with: - file: idea/gear/faucet/Dockerfile - push: true - tags: | - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-gear-idea-faucet:${{ needs.set-env-tag.outputs.environment }} - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-gear-idea-faucet:${{ needs.set-env-tag.outputs.environment }}-${{ github.sha }} - - build-meta-storage-image: - runs-on: ubuntu-latest - needs: [set-env-tag] - environment: ${{ needs.set-env-tag.outputs.environment }} - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Check if should be built - id: check - uses: './.github/actions/should-be-built' - with: - patterns: 'meta-storage,all,backend' - input: ${{ github.event.inputs.service }} - event_name: ${{ github.event_name }} - - - name: Log in to the github container registry - if: ${{ steps.check.outputs.ok == 'true' }} - uses: docker/login-action@master - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push "gear-idea-meta-storage" image - if: ${{ steps.check.outputs.ok == 'true' }} - uses: docker/build-push-action@master - with: - file: idea/gear/meta-storage/Dockerfile - push: true - tags: | - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-gear-idea-meta-storage:${{ needs.set-env-tag.outputs.environment }} - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-gear-idea-meta-storage:${{ needs.set-env-tag.outputs.environment }}-${{ github.sha }} - - build-squid-image: - runs-on: ubuntu-latest - needs: [set-env-tag] - environment: ${{ needs.set-env-tag.outputs.environment }} - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Check if should be built - id: check - uses: './.github/actions/should-be-built' - with: - patterns: 'squid,all,backend' - input: ${{ github.event.inputs.service }} - event_name: ${{ github.event_name }} - - - name: Log in to the github container registry - if: ${{ steps.check.outputs.ok == 'true' }} - uses: docker/login-action@master - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push "gear-idea-squid" image - if: ${{ steps.check.outputs.ok == 'true' }} - uses: docker/build-push-action@master - with: - file: idea/gear/squid/Dockerfile - push: true - tags: | - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-gear-idea-squid:${{ needs.set-env-tag.outputs.environment }} - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-gear-idea-squid:${{ needs.set-env-tag.outputs.environment }}-${{ github.sha }} - - build-explorer-image: - runs-on: ubuntu-latest - needs: [set-env-tag] - environment: ${{ needs.set-env-tag.outputs.environment }} - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Check if should be built - id: check - uses: './.github/actions/should-be-built' - with: - patterns: 'explorer,all,backend' - input: ${{ github.event.inputs.service }} - event_name: ${{ github.event_name }} - - - name: Log in to the github container registry - if: ${{ steps.check.outputs.ok == 'true' }} - uses: docker/login-action@master - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push "gear-idea-explorer" image - if: ${{ steps.check.outputs.ok == 'true' }} - uses: docker/build-push-action@master - with: - file: idea/gear/explorer/Dockerfile - push: true - tags: | - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-gear-idea-explorer:${{ needs.set-env-tag.outputs.environment }} - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-gear-idea-explorer:${{ needs.set-env-tag.outputs.environment }}-${{ github.sha }} - deploy-to-k8s: - needs: - [ - set-env-tag, - build-frontend-image, - build-faucet-image, - build-meta-storage-image, - build-squid-image, - build-explorer-image, - ] + needs: [build-frontend-image] runs-on: ubuntu-latest steps: @@ -328,30 +94,8 @@ jobs: - name: Get deployment variables id: deployment_vars run: | - if [ "${{ needs.set-env-tag.outputs.environment }}" == "production" ]; then - echo "namespace=prod-idea" >> $GITHUB_OUTPUT - echo "deployments=squid-testnet-v2 squid-mainnet-v2 explorer faucet frontend-nginx meta-storage-main" >> $GITHUB_OUTPUT - else - echo "namespace=dev-1" >> $GITHUB_OUTPUT - if [ "${{ github.event_name }}" != "workflow_dispatch" || "${{ github.event.inputs.service }}" == "all" ]; then - echo "deployments=squid-testnet-v2 explorer frontend-nginx meta-storage faucet" >> $GITHUB_OUTPUT - elif [ "${{ github.event.inputs.service }}" == "backend" ]; then - echo "deployments=squid-testnet-v2 explorer meta-storage faucet" >> $GITHUB_OUTPUT - elif [ "${{ github.event.inputs.service }}" == "frontend" ]; then - echo "deployments=frontend-nginx" >> $GITHUB_OUTPUT - elif [ "${{ github.event.inputs.service }}" == "faucet" ]; then - echo "deployments=faucet" >> $GITHUB_OUTPUT - elif [ "${{ github.event.inputs.service }}" == "meta-storage" ]; then - echo "deployments=meta-storage" >> $GITHUB_OUTPUT - elif [ "${{ github.event.inputs.service }}" == "explorer" ]; then - echo "deployments=explorer" >> $GITHUB_OUTPUT - elif [ "${{ github.event.inputs.service }}" == "squid" ]; then - echo "deployments=squid-testnet-v2" >> $GITHUB_OUTPUT - else - echo "deployments=" >> $GITHUB_OUTPUT - fi - fi - + echo "namespace=prod-idea" >> $GITHUB_OUTPUT + echo "deployments=frontend-temp" >> $GITHUB_OUTPUT - name: Deploy to k8s uses: sergeyfilyanin/kubectl-aws-eks@master with: diff --git a/idea/gear/frontend/Dockerfile b/idea/gear/frontend/Dockerfile index 52bada34b..ae6f9d11d 100644 --- a/idea/gear/frontend/Dockerfile +++ b/idea/gear/frontend/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine AS builder +FROM node:20-slim AS builder WORKDIR /src diff --git a/idea/gear/frontend/package.json b/idea/gear/frontend/package.json index b785e3920..710bf533a 100644 --- a/idea/gear/frontend/package.json +++ b/idea/gear/frontend/package.json @@ -10,6 +10,7 @@ "clean": "echo \"clean gear-idea-frontend\" && rm -rf dist" }, "dependencies": { + "@base-ui-components/react": "^1.0.0-beta.7", "@gear-js/api": "0.44.2", "@gear-js/react-hooks": "*", "@gear-js/ui": "*", diff --git a/idea/gear/frontend/src/features/code/ui/code-card/code-card.module.scss b/idea/gear/frontend/src/features/code/ui/code-card/code-card.module.scss index 0167d8f68..ae7956427 100644 --- a/idea/gear/frontend/src/features/code/ui/code-card/code-card.module.scss +++ b/idea/gear/frontend/src/features/code/ui/code-card/code-card.module.scss @@ -1,76 +1,84 @@ -@use '@gear-js/ui/mixins' as *; -@use '@/shared/assets/styles/shared' as *; -@use '@/shared/assets/styles/mixins' as *; -@use '@/shared/assets/styles/variables' as *; -@use '@/shared/assets/styles/animations' as *; +@use '@gear-js/ui/mixins' as *; '@gear-js/ui/mixins' as *; @use '@/共享/资产/样式/共享' as *; as *; +@use '@/共享/资产/样式/共享'作为 *;'@/共享/资产/样式/共享'作为 *; +@use '@/共享/资产/样式/混合'相对的 +@use '@/共享/资产/样式/变量'作为 *; +@use '@/共享/资产/样式/动画'作为 *; -.horizontalCodeCard { - @include transition(background-color); +。地平线​​ntalCodeCard { + @include过渡(background-color); - display: flex; - position: relative; - border-radius: toRem(16); - background-color: $bgColor4; - - &:hover { - background-color: $gray100; +展示: 弯曲; +位置:相对的; +徘徊边界半径(16);边界半径: 托雷姆(16); +背景颜色:$bgColor4;背景颜色:$bgColor4; - .content::after { - background: radial-gradient(50% 50% at 50% 50%, rgba($successColor, 0.45) 25%, rgba(24, 24, 27, 0) 100%); +&:悬停{hover { +背景颜色: $gray100; +ㄓ +。内容::后 { +背景: 径向渐变(50% 50%在50% 50%, RGBA($成功颜色,0.45) 25%, RGBA(24, 24, 27, 0) 100%); } } } -.content { - flex: 1 1; - padding: toRem(24); - overflow: hidden; - position: relative; - - &::after { - content: ''; - position: absolute; - right: -40%; - bottom: -100%; - width: 100%; - height: 200%; - background: radial-gradient(50% 50% at 50% 50%, rgba($successColor, 0.23) 0%, rgba(24, 24, 27, 0) 100%); +。内容 { +弯曲:1 1; +填充: 托雷姆(24); +溢出: 隐; +位置: 相对的; + +&::后 { +内容:'';'';'';''; +位置: 绝对; +正确的:-40%;-40%;-40%;-40%; +底部:-100%;-100%;-100%;-100%;-100%;-100%;-100%;-100%;-100%;-100%;-100%;-100%; +宽度:100%;100%;100%;100%; +高度:200%;200%;200%;200%; +背景: 径向渐变(50% 50% at 50% 50%, RGBA($successColor, 0.23) 0%, RGBA(24, 24, 27, 0) 100%);50% 50% at 50% 50%, RGBA($successColor, 0.23) 0%, RGBA(24, 24, 27, 0) 100%);50% 50% at 50% 50%, RGBA($successColor, 0.23) 0%, RGBA(24, 24, 27, 0) 100%);50% 50% at 50% 50%, RGBA($successColor, 0.23) 0%, RGBA(24, 24, 27, 0) 100%); } - .name { - @include transition; - display: block; - max-width: 60%; - overflow: hidden; - text-overflow: ellipsis; - font-size: $fontSizeBig; - font-family: 'Kanit'; - font-weight: 600; - line-height: 1.3; - margin-right: $margin; - margin-bottom: 10px; - position: relative; - z-index: 1; - - &:hover { - opacity: 0.5; +。关联 { +@包括过渡;@includetransition;包括过渡;@include过渡; + +右边距: $margin; +边距底部: 10像素;10像素;10像素;10像素; + +展示: 弯曲; +对齐项目: 中心; +差距:8像素;8像素;8像素;8像素; + +位置: 相对的; +z 索引:1;1;1;1; + +&:徘徊 { +不透明度:0.5;0.5; } } +。姓名 { +最大宽度:60%;60%; +溢出: 隐; +文本溢出: 省略; +字体大小:$fontSizeBig; +字体系列:'卡尼特'; +字体粗细:600; +行高:1.3; + } + .otherInfo { - display: flex; - position: relative; +展示: 弯曲; +位置:相对; } - .codeId { - z-index: 2; - margin-bottom: 10px; +.codeIdz 索引.codeId { +z 索引:2;z 索引: 2; +底部边距:10像素;边距底部:10像素; } - .otherInfo { - @include childrenMargin($margin, right); +.otherInfo {.otherInfo { +@include儿童保证金($margin, 右);@include儿童保证金($margin, 右); - z-index: 4; +z 索引:4;z 索引:4; } } diff --git a/idea/gear/frontend/src/features/code/ui/code-card/code-card.tsx b/idea/gear/frontend/src/features/code/ui/code-card/code-card.tsx index 599576c74..fba403ef4 100644 --- a/idea/gear/frontend/src/features/code/ui/code-card/code-card.tsx +++ b/idea/gear/frontend/src/features/code/ui/code-card/code-card.tsx @@ -1,6 +1,7 @@ import { Link, generatePath } from 'react-router-dom'; import { LocalCode } from '@/features/local-indexer'; +import { isVftCode, VftTag } from '@/features/vft-standard'; import CreateProgramSVG from '@/shared/assets/images/actions/create-program.svg?react'; import RelatedrelatedProgramsSVG from '@/shared/assets/images/actions/related-programs.svg?react'; import { absoluteRoutes, routes } from '@/shared/config'; @@ -24,8 +25,9 @@ function CodeCard({ code }: Props) { return (
- - {name || 'Code'} + + {name || 'Code'} + {isVftCode(code.id) && } {'timestamp' in code && ( diff --git a/idea/gear/frontend/src/features/program/ui/program-card/program-card.module.scss b/idea/gear/frontend/src/features/program/ui/program-card/program-card.module.scss index a0683b9cd..abc6359f2 100644 --- a/idea/gear/frontend/src/features/program/ui/program-card/program-card.module.scss +++ b/idea/gear/frontend/src/features/program/ui/program-card/program-card.module.scss @@ -39,6 +39,14 @@ .link { @include transition; + + margin-right: $margin; + margin-bottom: 10px; + + display: flex; + align-items: center; + gap: 8px; + position: relative; z-index: 1; @@ -77,8 +85,6 @@ font-family: 'Kanit'; font-weight: 600; line-height: 1.3; - margin-right: $margin; - margin-bottom: 10px; } } diff --git a/idea/gear/frontend/src/features/program/ui/program-card/program-card.tsx b/idea/gear/frontend/src/features/program/ui/program-card/program-card.tsx index 902b19e7e..c49c7f6d0 100644 --- a/idea/gear/frontend/src/features/program/ui/program-card/program-card.tsx +++ b/idea/gear/frontend/src/features/program/ui/program-card/program-card.tsx @@ -2,6 +2,7 @@ import { clsx } from 'clsx'; import { Link, generatePath } from 'react-router-dom'; import { LocalProgram } from '@/features/local-indexer'; +import { isVftCode, VftTag } from '@/features/vft-standard'; import { IssueVoucher, VoucherBadge } from '@/features/voucher'; import sendSVG from '@/shared/assets/images/actions/send.svg?react'; import { absoluteRoutes } from '@/shared/config'; @@ -32,6 +33,7 @@ const ProgramCard = ({ program, vertical }: Props) => {

{name}

+ {program.codeId && isVftCode(program.codeId) && }
diff --git a/idea/gear/frontend/src/features/vft-standard/assets/extended_vft.idl b/idea/gear/frontend/src/features/vft-standard/assets/extended_vft.idl new file mode 100644 index 000000000..c252e14ba --- /dev/null +++ b/idea/gear/frontend/src/features/vft-standard/assets/extended_vft.idl @@ -0,0 +1,48 @@ +constructor { + New : (name: str, symbol: str, decimals: u8); +}; + +service Vft { + Burn : (from: actor_id, value: u256) -> bool; + GrantAdminRole : (to: actor_id) -> null; + GrantBurnerRole : (to: actor_id) -> null; + GrantMinterRole : (to: actor_id) -> null; + Mint : (to: actor_id, value: u256) -> bool; + RevokeAdminRole : (from: actor_id) -> null; + RevokeBurnerRole : (from: actor_id) -> null; + RevokeMinterRole : (from: actor_id) -> null; + Approve : (spender: actor_id, value: u256) -> bool; + Transfer : (to: actor_id, value: u256) -> bool; + TransferFrom : (from: actor_id, to: actor_id, value: u256) -> bool; + query Admins : () -> vec actor_id; + query Burners : () -> vec actor_id; + query Minters : () -> vec actor_id; + query Allowance : (owner: actor_id, spender: actor_id) -> u256; + query BalanceOf : (account: actor_id) -> u256; + query Decimals : () -> u8; + query Name : () -> str; + query Symbol : () -> str; + query TotalSupply : () -> u256; + + events { + Minted: struct { + to: actor_id, + value: u256, + }; + Burned: struct { + from: actor_id, + value: u256, + }; + Approval: struct { + owner: actor_id, + spender: actor_id, + value: u256, + }; + Transfer: struct { + from: actor_id, + to: actor_id, + value: u256, + }; + } +}; + diff --git a/idea/gear/frontend/src/features/vft-standard/consts.ts b/idea/gear/frontend/src/features/vft-standard/consts.ts new file mode 100644 index 000000000..a6a3ebd46 --- /dev/null +++ b/idea/gear/frontend/src/features/vft-standard/consts.ts @@ -0,0 +1,3 @@ +const VFT_CODE_IDS = ['0x17a3faf3760254f07b198559a01cf97d7cbf35d4b149293dcc1b8f1edac9e10b']; + +export { VFT_CODE_IDS }; diff --git a/idea/gear/frontend/src/features/vft-standard/hooks.ts b/idea/gear/frontend/src/features/vft-standard/hooks.ts new file mode 100644 index 000000000..d32ed1fa4 --- /dev/null +++ b/idea/gear/frontend/src/features/vft-standard/hooks.ts @@ -0,0 +1,37 @@ +import { HexString } from '@gear-js/api'; +import { useAccount, useProgram, useProgramQuery } from '@gear-js/react-hooks'; + +import { SailsProgram } from './sails'; + +function useVftProgram(id: HexString | undefined) { + return useProgram({ id, library: SailsProgram }); +} + +function useVftDecimals(id: HexString | undefined) { + const { data: program } = useVftProgram(id); + + return useProgramQuery({ program, serviceName: 'vft', functionName: 'decimals', args: [] }); +} + +function useVftRoles(id: HexString | undefined) { + const { data: program } = useVftProgram(id); + + const admins = useProgramQuery({ program, serviceName: 'vft', functionName: 'admins', args: [] }); + const minters = useProgramQuery({ program, serviceName: 'vft', functionName: 'minters', args: [] }); + const burners = useProgramQuery({ program, serviceName: 'vft', functionName: 'burners', args: [] }); + + return { admins, minters, burners }; +} + +function useAccountRole(id: HexString | undefined) { + const { account } = useAccount(); + const { admins, minters, burners } = useVftRoles(id); + + const isAdmin = account ? admins.data?.includes(account.decodedAddress) : false; + const isMinter = account ? minters.data?.includes(account.decodedAddress) : false; + const isBurner = account ? burners.data?.includes(account.decodedAddress) : false; + + return { isAdmin, isMinter, isBurner }; +} + +export { useVftProgram, useVftDecimals, useVftRoles, useAccountRole }; diff --git a/idea/gear/frontend/src/features/vft-standard/index.ts b/idea/gear/frontend/src/features/vft-standard/index.ts new file mode 100644 index 000000000..42cf43aae --- /dev/null +++ b/idea/gear/frontend/src/features/vft-standard/index.ts @@ -0,0 +1,4 @@ +import { VftTag, Vft } from './ui'; +import { isVftCode } from './utils'; + +export { VftTag, isVftCode, Vft }; diff --git a/idea/gear/frontend/src/features/vft-standard/sails.ts b/idea/gear/frontend/src/features/vft-standard/sails.ts new file mode 100644 index 000000000..cb6408048 --- /dev/null +++ b/idea/gear/frontend/src/features/vft-standard/sails.ts @@ -0,0 +1,442 @@ +/* eslint-disable */ + +import { GearApi, BaseGearProgram, HexString } from '@gear-js/api'; +import { TypeRegistry } from '@polkadot/types'; +import { + TransactionBuilder, + ActorId, + QueryBuilder, + getServiceNamePrefix, + getFnNamePrefix, + ZERO_ADDRESS, +} from 'sails-js'; + +export class SailsProgram { + public readonly registry: TypeRegistry; + public readonly vft: Vft; + private _program?: BaseGearProgram; + + constructor( + public api: GearApi, + programId?: `0x${string}`, + ) { + const types: Record = {}; + + this.registry = new TypeRegistry(); + this.registry.setKnownTypes({ types }); + this.registry.register(types); + if (programId) { + this._program = new BaseGearProgram(programId, api); + } + + this.vft = new Vft(this); + } + + public get programId(): `0x${string}` { + if (!this._program) throw new Error(`Program ID is not set`); + return this._program.id; + } + + newCtorFromCode( + code: Uint8Array | Buffer | HexString, + name: string, + $symbol: string, + decimals: number, + ): TransactionBuilder { + const builder = new TransactionBuilder( + this.api, + this.registry, + 'upload_program', + null, + 'New', + [name, $symbol, decimals], + '(String, String, u8)', + 'String', + code, + async (programId) => { + this._program = await BaseGearProgram.new(programId, this.api); + }, + ); + return builder; + } + + newCtorFromCodeId(codeId: `0x${string}`, name: string, $symbol: string, decimals: number) { + const builder = new TransactionBuilder( + this.api, + this.registry, + 'create_program', + null, + 'New', + [name, $symbol, decimals], + '(String, String, u8)', + 'String', + codeId, + async (programId) => { + this._program = await BaseGearProgram.new(programId, this.api); + }, + ); + return builder; + } +} + +export class Vft { + constructor(private _program: SailsProgram) {} + + public burn($from: ActorId, value: number | string | bigint): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + 'Vft', + 'Burn', + [$from, value], + '([u8;32], U256)', + 'bool', + this._program.programId, + ); + } + + public grantAdminRole(to: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + 'Vft', + 'GrantAdminRole', + to, + '[u8;32]', + 'Null', + this._program.programId, + ); + } + + public grantBurnerRole(to: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + 'Vft', + 'GrantBurnerRole', + to, + '[u8;32]', + 'Null', + this._program.programId, + ); + } + + public grantMinterRole(to: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + 'Vft', + 'GrantMinterRole', + to, + '[u8;32]', + 'Null', + this._program.programId, + ); + } + + public mint(to: ActorId, value: number | string | bigint): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + 'Vft', + 'Mint', + [to, value], + '([u8;32], U256)', + 'bool', + this._program.programId, + ); + } + + public revokeAdminRole($from: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + 'Vft', + 'RevokeAdminRole', + $from, + '[u8;32]', + 'Null', + this._program.programId, + ); + } + + public revokeBurnerRole($from: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + 'Vft', + 'RevokeBurnerRole', + $from, + '[u8;32]', + 'Null', + this._program.programId, + ); + } + + public revokeMinterRole($from: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + 'Vft', + 'RevokeMinterRole', + $from, + '[u8;32]', + 'Null', + this._program.programId, + ); + } + + public approve(spender: ActorId, value: number | string | bigint): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + 'Vft', + 'Approve', + [spender, value], + '([u8;32], U256)', + 'bool', + this._program.programId, + ); + } + + public transfer(to: ActorId, value: number | string | bigint): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + 'Vft', + 'Transfer', + [to, value], + '([u8;32], U256)', + 'bool', + this._program.programId, + ); + } + + public transferFrom($from: ActorId, to: ActorId, value: number | string | bigint): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + 'Vft', + 'TransferFrom', + [$from, to, value], + '([u8;32], [u8;32], U256)', + 'bool', + this._program.programId, + ); + } + + public admins(): QueryBuilder> { + return new QueryBuilder>( + this._program.api, + this._program.registry, + this._program.programId, + 'Vft', + 'Admins', + null, + null, + 'Vec<[u8;32]>', + ); + } + + public burners(): QueryBuilder> { + return new QueryBuilder>( + this._program.api, + this._program.registry, + this._program.programId, + 'Vft', + 'Burners', + null, + null, + 'Vec<[u8;32]>', + ); + } + + public minters(): QueryBuilder> { + return new QueryBuilder>( + this._program.api, + this._program.registry, + this._program.programId, + 'Vft', + 'Minters', + null, + null, + 'Vec<[u8;32]>', + ); + } + + public allowance(owner: ActorId, spender: ActorId): QueryBuilder { + return new QueryBuilder( + this._program.api, + this._program.registry, + this._program.programId, + 'Vft', + 'Allowance', + [owner, spender], + '([u8;32], [u8;32])', + 'U256', + ); + } + + public balanceOf(account: ActorId): QueryBuilder { + return new QueryBuilder( + this._program.api, + this._program.registry, + this._program.programId, + 'Vft', + 'BalanceOf', + account, + '[u8;32]', + 'U256', + ); + } + + public decimals(): QueryBuilder { + return new QueryBuilder( + this._program.api, + this._program.registry, + this._program.programId, + 'Vft', + 'Decimals', + null, + null, + 'u8', + ); + } + + public name(): QueryBuilder { + return new QueryBuilder( + this._program.api, + this._program.registry, + this._program.programId, + 'Vft', + 'Name', + null, + null, + 'String', + ); + } + + public symbol(): QueryBuilder { + return new QueryBuilder( + this._program.api, + this._program.registry, + this._program.programId, + 'Vft', + 'Symbol', + null, + null, + 'String', + ); + } + + public totalSupply(): QueryBuilder { + return new QueryBuilder( + this._program.api, + this._program.registry, + this._program.programId, + 'Vft', + 'TotalSupply', + null, + null, + 'U256', + ); + } + + public subscribeToMintedEvent( + callback: (data: { to: ActorId; value: number | string | bigint }) => void | Promise, + ): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Vft' && getFnNamePrefix(payload) === 'Minted') { + callback( + this._program.registry + .createType('(String, String, {"to":"[u8;32]","value":"U256"})', message.payload)[2] + .toJSON() as unknown as { to: ActorId; value: number | string | bigint }, + ); + } + }); + } + + public subscribeToBurnedEvent( + callback: (data: { from: ActorId; value: number | string | bigint }) => void | Promise, + ): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Vft' && getFnNamePrefix(payload) === 'Burned') { + callback( + this._program.registry + .createType('(String, String, {"from":"[u8;32]","value":"U256"})', message.payload)[2] + .toJSON() as unknown as { from: ActorId; value: number | string | bigint }, + ); + } + }); + } + + public subscribeToApprovalEvent( + callback: (data: { owner: ActorId; spender: ActorId; value: number | string | bigint }) => void | Promise, + ): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Vft' && getFnNamePrefix(payload) === 'Approval') { + callback( + this._program.registry + .createType('(String, String, {"owner":"[u8;32]","spender":"[u8;32]","value":"U256"})', message.payload)[2] + .toJSON() as unknown as { owner: ActorId; spender: ActorId; value: number | string | bigint }, + ); + } + }); + } + + public subscribeToTransferEvent( + callback: (data: { from: ActorId; to: ActorId; value: number | string | bigint }) => void | Promise, + ): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Vft' && getFnNamePrefix(payload) === 'Transfer') { + callback( + this._program.registry + .createType('(String, String, {"from":"[u8;32]","to":"[u8;32]","value":"U256"})', message.payload)[2] + .toJSON() as unknown as { from: ActorId; to: ActorId; value: number | string | bigint }, + ); + } + }); + } +} diff --git a/idea/gear/frontend/src/features/vft-standard/ui/index.ts b/idea/gear/frontend/src/features/vft-standard/ui/index.ts new file mode 100644 index 000000000..f0873b7fe --- /dev/null +++ b/idea/gear/frontend/src/features/vft-standard/ui/index.ts @@ -0,0 +1,4 @@ +import { Vft } from './vft'; +import { VftTag } from './vft-tag'; + +export { Vft, VftTag }; diff --git a/idea/gear/frontend/src/features/vft-standard/ui/vft-tag/index.ts b/idea/gear/frontend/src/features/vft-standard/ui/vft-tag/index.ts new file mode 100644 index 000000000..bbd0f71a7 --- /dev/null +++ b/idea/gear/frontend/src/features/vft-standard/ui/vft-tag/index.ts @@ -0,0 +1,3 @@ +import { VftTag } from './vft-tag'; + +export { VftTag }; diff --git a/idea/gear/frontend/src/features/vft-standard/ui/vft-tag/vft-tag.module.scss b/idea/gear/frontend/src/features/vft-standard/ui/vft-tag/vft-tag.module.scss new file mode 100644 index 000000000..89042e213 --- /dev/null +++ b/idea/gear/frontend/src/features/vft-standard/ui/vft-tag/vft-tag.module.scss @@ -0,0 +1,21 @@ +.tag { + text-align: center; + + font-weight: 500; + color: rgba(#fff, 0.6); + + border: 1px solid rgba(#fff, 0.6); + border-radius: 4px; + + &.small { + padding: 0 4px; + + font-size: 10px; + } + + &.medium { + padding: 0 6px; + + font-size: 12px; + } +} diff --git a/idea/gear/frontend/src/features/vft-standard/ui/vft-tag/vft-tag.tsx b/idea/gear/frontend/src/features/vft-standard/ui/vft-tag/vft-tag.tsx new file mode 100644 index 000000000..df1f607b6 --- /dev/null +++ b/idea/gear/frontend/src/features/vft-standard/ui/vft-tag/vft-tag.tsx @@ -0,0 +1,13 @@ +import { cx } from '@/shared/helpers'; + +import styles from './vft-tag.module.scss'; + +type Props = { + size?: 'medium' | 'small'; +}; + +function VftTag({ size = 'small' }: Props) { + return VFT; +} + +export { VftTag }; diff --git a/idea/gear/frontend/src/features/vft-standard/ui/vft/index.ts b/idea/gear/frontend/src/features/vft-standard/ui/vft/index.ts new file mode 100644 index 000000000..7f9da2146 --- /dev/null +++ b/idea/gear/frontend/src/features/vft-standard/ui/vft/index.ts @@ -0,0 +1,3 @@ +import { Vft } from './vft'; + +export { Vft }; diff --git a/idea/gear/frontend/src/features/vft-standard/ui/vft/vft-account-overview.module.scss b/idea/gear/frontend/src/features/vft-standard/ui/vft/vft-account-overview.module.scss new file mode 100644 index 000000000..d34e87914 --- /dev/null +++ b/idea/gear/frontend/src/features/vft-standard/ui/vft/vft-account-overview.module.scss @@ -0,0 +1,59 @@ +@use '@/shared/assets/styles/variables' as *; + +.accountOverview { + display: flex; + flex-direction: column; + gap: 20px; +} + +.title { + font-size: 20px; + font-weight: 600; + line-height: 1.3; + color: $textColor; + margin: 0; +} + +.info { + display: flex; + flex-direction: column; + gap: 12px; +} + +.infoItem { + display: flex; + align-items: center; + justify-content: space-between; +} + +.label { + font-size: 14px; + color: $gray700; +} + +.value { + font-size: 14px; + font-weight: 500; + color: $textColor; +} + +.roles { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.roleBadge { + padding: 2px 8px; + font-size: 12px; + font-weight: 500; + color: rgba($successColor, 1); + background-color: rgba($successColor, 0.1); + border: 1px solid rgba($successColor, 0.2); + border-radius: 4px; +} + +.noRoles { + font-size: 14px; + color: $gray600; +} \ No newline at end of file diff --git a/idea/gear/frontend/src/features/vft-standard/ui/vft/vft-account-overview.tsx b/idea/gear/frontend/src/features/vft-standard/ui/vft/vft-account-overview.tsx new file mode 100644 index 000000000..03bd32259 --- /dev/null +++ b/idea/gear/frontend/src/features/vft-standard/ui/vft/vft-account-overview.tsx @@ -0,0 +1,73 @@ +import { HexString } from '@gear-js/api'; +import { useAccount, useProgramQuery } from '@gear-js/react-hooks'; +import { ZERO_ADDRESS } from 'sails-js'; + +import { Box } from '@/shared/ui'; + +import { useAccountRole, useVftProgram } from '../../hooks'; + +import styles from './vft-account-overview.module.scss'; + +type Props = { + id: HexString | undefined; +}; + +function VftAccountOverview({ id }: Props) { + const { account } = useAccount(); + const { data: program } = useVftProgram(id); + + const balance = useProgramQuery({ + program, + serviceName: 'vft', + functionName: 'balanceOf', + args: [account?.decodedAddress || ZERO_ADDRESS], + query: { enabled: Boolean(account) }, + }); + + const { isAdmin, isMinter, isBurner } = useAccountRole(id); + + const renderRoles = () => { + const roles = []; + + if (isAdmin) + roles.push( + + Admin + , + ); + if (isMinter) + roles.push( + + Minter + , + ); + if (isBurner) + roles.push( + + Burner + , + ); + + return roles.length > 0 ? roles : No roles; + }; + + return ( + +

Account Overview

+ +
+
+ Your Balance: + {balance.data || '0'} +
+ +
+ Your Roles: +
{renderRoles()}
+
+
+
+ ); +} + +export { VftAccountOverview }; diff --git a/idea/gear/frontend/src/features/vft-standard/ui/vft/vft-account-overview.tsx.module.scss b/idea/gear/frontend/src/features/vft-standard/ui/vft/vft-account-overview.tsx.module.scss new file mode 100644 index 000000000..c0c54a023 --- /dev/null +++ b/idea/gear/frontend/src/features/vft-standard/ui/vft/vft-account-overview.tsx.module.scss @@ -0,0 +1,59 @@ +@use '@/shared/assets/styles/variables' as *; + +.accountOverview { + display: flex; + flex-direction: column; + gap: 20px; +} + +.title { + font-size: 20px; + font-weight: 600; + line-height: 1.3; + color: $textColor; + margin: 0; +} + +.info { + display: flex; + flex-direction: column; + gap: 12px; +} + +.infoItem { + display: flex; + align-items: center; + justify-content: space-between; +} + +.label { + font-size: 14px; + color: $gray700; +} + +.value { + font-size: 14px; + font-weight: 500; + color: $textColor; +} + +.roles { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.roleBadge { + padding: 2px 8px; + font-size: 12px; + font-weight: 500; + color: rgba($successColor, 1); + background-color: rgba($successColor, 0.1); + border: 1px solid rgba($successColor, 0.2); + border-radius: 4px; +} + +.noRoles { + font-size: 14px; + color: $gray600; +} diff --git a/idea/gear/frontend/src/features/vft-standard/ui/vft/vft-actions.module.scss b/idea/gear/frontend/src/features/vft-standard/ui/vft/vft-actions.module.scss new file mode 100644 index 000000000..7badc4c8b --- /dev/null +++ b/idea/gear/frontend/src/features/vft-standard/ui/vft/vft-actions.module.scss @@ -0,0 +1,168 @@ +@use '@/shared/assets/styles/variables' as *; + +.Button { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + height: 32px; + padding: 0 12px; + margin: 0; + outline: 0; + border: 1px solid $gray100; + border-radius: 8px; + background-color: $bgColor5; + font-family: inherit; + font-size: 14px; + font-weight: 500; + line-height: 1; + color: $textColor; + user-select: none; + cursor: pointer; + transition: all 0.25s; + + &:hover { + background-color: $bgColorTertiary; + } + + &:active { + background-color: $bgColorTertiary; + } + + &[data-popup-open] { + background-color: $bgColorTertiary; + } + + &:focus-visible { + outline: 2px solid $successColor; + outline-offset: -1px; + } +} + +.ButtonIcon { + width: 10px; + height: 10px; + + path { + stroke: $gray700; + } +} + +.Positioner { + outline: 0; + z-index: 10; +} + +.Popup { + min-width: 180px; + padding: 4px; + border-radius: 12px; + background-color: $bgColor5; + box-shadow: + inset 0 1px 0 0 rgba(255, 255, 255, 0.05), + 0 20px 76px 0 rgba(0, 0, 0, 0.5); + transform-origin: var(--transform-origin); + transition: + transform 150ms, + opacity 150ms; + + &[data-starting-style], + &[data-ending-style] { + opacity: 0; + transform: scale(0.9); + } + + @media (prefers-color-scheme: light) { + outline: 1px solid var(--color-gray-200); + box-shadow: + 0 10px 15px -3px var(--color-gray-200), + 0 4px 6px -4px var(--color-gray-200); + } + + @media (prefers-color-scheme: dark) { + outline: 1px solid var(--color-gray-300); + outline-offset: -1px; + } +} + +.Arrow { + display: flex; + + &[data-side='top'] { + bottom: -8px; + rotate: 180deg; + } + + &[data-side='bottom'] { + top: -8px; + rotate: 0deg; + } + + &[data-side='left'] { + right: -13px; + rotate: 90deg; + } + + &[data-side='right'] { + left: -13px; + rotate: -90deg; + } +} + +.ArrowFill { + fill: canvas; +} + +.ArrowOuterStroke { + @media (prefers-color-scheme: light) { + fill: black; + } +} + +.ArrowInnerStroke { + @media (prefers-color-scheme: dark) { + fill: black; + } +} + +.Item { + outline: 0; + cursor: default; + user-select: none; + padding-block: 0.5rem; + padding-left: 1rem; + padding-right: 2rem; + display: flex; + font-size: 0.875rem; + line-height: 1rem; + background-color: transparent; + color: var(--color-gray-50); + + &[data-highlighted] { + z-index: 0; + position: relative; + color: #000; + } + + &[data-highlighted]::before { + content: ''; + z-index: -1; + position: absolute; + inset-block: 0; + inset-inline: 0.25rem; + border-radius: 0.25rem; + background-color: var(--color-gray-900); + } +} + +.Separator { + margin: 0.375rem 1rem; + height: 1px; + background-color: var(--color-gray-200); +} + +.modal { + display: flex; + flex-direction: column; + gap: 24px; +} diff --git a/idea/gear/frontend/src/features/vft-standard/ui/vft/vft-actions.tsx b/idea/gear/frontend/src/features/vft-standard/ui/vft/vft-actions.tsx new file mode 100644 index 000000000..ee0bd9294 --- /dev/null +++ b/idea/gear/frontend/src/features/vft-standard/ui/vft/vft-actions.tsx @@ -0,0 +1,123 @@ +import { Menu } from '@base-ui-components/react/menu'; +import { Button, Input, Modal } from '@gear-js/ui'; +import * as React from 'react'; +import { useState } from 'react'; + +import styles from './vft-actions.module.scss'; + +const ACTIONS = [ + { label: 'Burn' }, + { label: 'Mint' }, + { label: 'Transfer' }, + { label: 'Transfer From' }, + { label: 'Approve' }, +]; + +const READS = [{ label: 'Balance Of' }, { label: 'Allowance' }]; + +type MenuProps = { + triggerText: string; + items: { label: string; onClick: () => void }[]; +}; + +function ExampleMenu({ triggerText, items }: MenuProps) { + const renderItems = () => + items.map((item) => ( + } onClick={item.onClick}> + {item.label} + + )); + + return ( + + + {triggerText} + + + + + + + + + + {renderItems()} + + + + + ); +} + +function ArrowSvg(props: React.ComponentProps<'svg'>) { + return ( + + + + + + ); +} + +function ChevronDownIcon(props: React.ComponentProps<'svg'>) { + return ( + + + + ); +} + +function PlaceholderModal({ close }: { close: () => void }) { + return ( + +
+ + + +
+ +
+ + {isConfirmModalOpen && ( + setIsConfirmModalOpen(false)} + onSubmit={() => {}} + /> + )} + + {isGrantRoleModalOpen && setIsGrantRoleModalOpen(false)} />} + + ); +} + +export { VftRoles }; diff --git a/idea/gear/frontend/src/features/vft-standard/ui/vft/vft.module.scss b/idea/gear/frontend/src/features/vft-standard/ui/vft/vft.module.scss new file mode 100644 index 000000000..d35cb7f19 --- /dev/null +++ b/idea/gear/frontend/src/features/vft-standard/ui/vft/vft.module.scss @@ -0,0 +1,13 @@ +@use '@/shared/assets/styles/variables' as *; + +.container { + display: flex; + flex-direction: column; + gap: 32px; +} + +.header { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 32px; +} diff --git a/idea/gear/frontend/src/features/vft-standard/ui/vft/vft.tsx b/idea/gear/frontend/src/features/vft-standard/ui/vft/vft.tsx new file mode 100644 index 000000000..9adbb133c --- /dev/null +++ b/idea/gear/frontend/src/features/vft-standard/ui/vft/vft.tsx @@ -0,0 +1,29 @@ +import { HexString } from '@gear-js/api'; +import { Sails } from 'sails-js'; + +import { VftAccountOverview } from './vft-account-overview'; +import { VftEvents } from './vft-events'; +import { VftOverview } from './vft-overview'; +import { VftRoles } from './vft-roles'; +import styles from './vft.module.scss'; + +type Props = { + id: HexString | undefined; + sails: Sails | undefined; +}; + +function Vft({ id, sails }: Props) { + return ( +
+
+ + +
+ + + +
+ ); +} + +export { Vft }; diff --git a/idea/gear/frontend/src/features/vft-standard/utils.ts b/idea/gear/frontend/src/features/vft-standard/utils.ts new file mode 100644 index 000000000..0e2090c68 --- /dev/null +++ b/idea/gear/frontend/src/features/vft-standard/utils.ts @@ -0,0 +1,7 @@ +import { HexString } from '@gear-js/api'; + +import { VFT_CODE_IDS } from './consts'; + +const isVftCode = (codeId: HexString) => VFT_CODE_IDS.includes(codeId); + +export { isVftCode }; diff --git a/idea/gear/frontend/src/pages/program/program.tsx b/idea/gear/frontend/src/pages/program/program.tsx index 176397325..48400e2a3 100644 --- a/idea/gear/frontend/src/pages/program/program.tsx +++ b/idea/gear/frontend/src/pages/program/program.tsx @@ -9,6 +9,7 @@ import { ProgramMessages } from '@/features/message'; import { useMetadata, MetadataTable, isState } from '@/features/metadata'; import { ProgramStatus, ProgramTable, useProgram } from '@/features/program'; import { ProgramEvents, SailsPreview, useSails } from '@/features/sails'; +import { isVftCode, Vft, VftTag } from '@/features/vft-standard'; import { ProgramVouchers } from '@/features/voucher'; import { useModal } from '@/hooks'; import AddMetaSVG from '@/shared/assets/images/actions/addMeta.svg?react'; @@ -21,6 +22,7 @@ import { Box, UILink } from '@/shared/ui'; import styles from './program.module.scss'; const TABS = ['Metadata/Sails', 'Messages', 'Events', 'Vouchers']; +const VFT_TABS = ['VFT Studio', 'Metadata/Sails', 'Messages', 'Events', 'Vouchers']; type Params = { programId: HexString; @@ -35,6 +37,7 @@ const Program = () => { const { sails, isLoading: isSailsLoading, refetch: refetchSails } = useSails(program?.codeId); // commented out till code verified is fixed // const { data: isCodeVerified } = useIsCodeVerified(program?.codeId); + const isVft = program?.codeId ? isVftCode(program.codeId) : false; const isLoading = !isMetadataReady || isSailsLoading; const isAnyQuery = sails ? Object.values(sails.services).some(({ queries }) => isAnyKey(queries)) : false; @@ -67,8 +70,9 @@ const Program = () => { }); }; - const renderTabs = () => - TABS.map((tab, index) => ( + const renderTabs = () => { + const tabs = isVft ? VFT_TABS : TABS; + return tabs.map((tab, index) => ( )); + }; return (
{program &&

{getShortName(program.name || 'Program Name')}

} + {isVft && } {/* commented out till code verified is fixed */} {/* {isCodeVerified && } */} @@ -131,18 +137,36 @@ const Program = () => {
{renderTabs()}
- {tabIndex === 0 && - (sails ? ( - - - - ) : ( - - ))} - - {tabIndex === 1 && } - {tabIndex === 2 && !isSailsLoading && } - {tabIndex === 3 && } + {isVft ? ( + <> + {tabIndex === 0 && } + {tabIndex === 1 && + (sails ? ( + + + + ) : ( + + ))} + {tabIndex === 2 && } + {tabIndex === 3 && !isSailsLoading && } + {tabIndex === 4 && } + + ) : ( + <> + {tabIndex === 0 && + (sails ? ( + + + + ) : ( + + ))} + {tabIndex === 1 && } + {tabIndex === 2 && !isSailsLoading && } + {tabIndex === 3 && } + + )}
); diff --git a/idea/gear/frontend/src/shared/helpers/index.ts b/idea/gear/frontend/src/shared/helpers/index.ts index 4df4dc6ee..0d55c4e5a 100644 --- a/idea/gear/frontend/src/shared/helpers/index.ts +++ b/idea/gear/frontend/src/shared/helpers/index.ts @@ -2,6 +2,7 @@ import { GearApi, HexString } from '@gear-js/api'; import { Account, AlertContainerFactory } from '@gear-js/react-hooks'; import type { Event } from '@polkadot/types/interfaces'; import { isAndroid, isIOS } from '@react-aria/utils'; +import { clsx } from 'clsx'; import { ACCOUNT_ERRORS, NODE_ADRESS_URL_PARAM, FileTypes } from '@/shared/config'; @@ -9,6 +10,8 @@ import { getReplyErrorReason } from './error'; import { fetchWithGuard } from './fetch-with-guard'; import { isHexValid, isExists, isAccountAddressValid, isNumeric, asOptionalField } from './form'; +const cx = clsx; + const checkWallet = (account?: Account) => { if (!account) { throw new Error(ACCOUNT_ERRORS.WALLET_NOT_CONNECTED); @@ -133,6 +136,7 @@ const getErrorMessage = (error: unknown) => (error instanceof Error ? error.mess const isAnyKey = (value: Record) => Object.keys(value).length > 0; export { + cx, checkWallet, formatDate, readFileAsync, diff --git a/yarn.lock b/yarn.lock index 60a216882..3e41c672f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1713,6 +1713,28 @@ __metadata: languageName: node linkType: hard +"@base-ui-components/react@npm:^1.0.0-beta.7": + version: 1.0.0-beta.7 + resolution: "@base-ui-components/react@npm:1.0.0-beta.7" + dependencies: + "@babel/runtime": "npm:^7.28.4" + "@base-ui-components/utils": "npm:0.2.1" + "@floating-ui/react-dom": "npm:^2.1.6" + "@floating-ui/utils": "npm:^0.2.10" + reselect: "npm:^5.1.1" + tabbable: "npm:^6.3.0" + use-sync-external-store: "npm:^1.6.0" + peerDependencies: + "@types/react": ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/7836d7f26ead3e13a4331094076879e3dded96c865f2e83014ed68606e41434ab43e1363072d870959b19a3b743d9ffeb495a779c9f3d5485d1c94ec9ecd599b + languageName: node + linkType: hard + "@base-ui-components/utils@npm:0.2.0": version: 0.2.0 resolution: "@base-ui-components/utils@npm:0.2.0" @@ -1732,6 +1754,25 @@ __metadata: languageName: node linkType: hard +"@base-ui-components/utils@npm:0.2.1": + version: 0.2.1 + resolution: "@base-ui-components/utils@npm:0.2.1" + dependencies: + "@babel/runtime": "npm:^7.28.4" + "@floating-ui/utils": "npm:^0.2.10" + reselect: "npm:^5.1.1" + use-sync-external-store: "npm:^1.6.0" + peerDependencies: + "@types/react": ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/96d845b709ecd9f99dcce1a170eeb2527a6ebc02aa921d1cc08284c5a8685c47d5a3ec3c9f748f7f731d673b8d2cb5a4f0d51f1a0fedafcfbbe384dcfc1a850c + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -15851,6 +15892,7 @@ __metadata: version: 0.0.0-use.local resolution: "gear-idea-frontend@workspace:idea/gear/frontend" dependencies: + "@base-ui-components/react": "npm:^1.0.0-beta.7" "@gear-js/api": "npm:0.44.2" "@gear-js/frontend-configs": "npm:*" "@gear-js/react-hooks": "npm:*"