├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── package.json ├── postcss.config.js ├── src ├── App.less ├── App.tsx ├── api.tsx ├── assets │ └── images │ │ ├── bg.svg │ │ ├── full_logo.svg │ │ ├── img-1.svg │ │ ├── img-10.svg │ │ ├── img-2.svg │ │ ├── img-3.svg │ │ ├── img-4.svg │ │ ├── img-5.svg │ │ ├── img-6.svg │ │ ├── img-7.svg │ │ ├── img-8.svg │ │ ├── img-9.svg │ │ └── logo.svg ├── chia-util.tsx ├── components │ ├── Donate.less │ ├── Donate.tsx │ ├── Login.tsx │ ├── TokenSelect.tsx │ └── UserInfo.tsx ├── favicon.svg ├── index.css ├── logo.svg ├── main.tsx └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── vite.config.ts └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: [ 7 | 'eslint:recommended', 8 | 'plugin:react/recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:react/jsx-runtime', 11 | ], 12 | parser: '@typescript-eslint/parser', 13 | parserOptions: { 14 | ecmaFeatures: { 15 | jsx: true, 16 | }, 17 | ecmaVersion: 12, 18 | sourceType: 'module', 19 | }, 20 | plugins: ['react', '@typescript-eslint'], 21 | rules: {}, 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | *.log 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 GobyWallet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # donation site 2 | 3 | A simple donation site to show how to interact with Goby. 4 | 5 | ## Quickstart 6 | 7 | ``` 8 | yarn install 9 | 10 | # preview 11 | yarn run dev 12 | 13 | # build 14 | yarn run build 15 | ``` 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | Goby 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gobywallet-demo", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "tsc && vite build", 7 | "preview": "vite preview" 8 | }, 9 | "dependencies": { 10 | "antd": "^4.18.3", 11 | "autoprefixer": "^10.4.2", 12 | "axios": "^0.25.0", 13 | "bech32": "^2.0.0", 14 | "bignumber.js": "^9.0.2", 15 | "buffer": "^6.0.3", 16 | "clvm": "^1.0.9", 17 | "postcss": "^8.4.5", 18 | "react": "^17.0.2", 19 | "react-dom": "^17.0.2", 20 | "tailwindcss": "^3.0.15" 21 | }, 22 | "devDependencies": { 23 | "@types/antd": "^1.0.0", 24 | "@types/autoprefixer": "^10.2.0", 25 | "@types/less": "^3.0.3", 26 | "@types/node": "^17.0.9", 27 | "@types/react": "^17.0.33", 28 | "@types/react-dom": "^17.0.10", 29 | "@types/tailwindcss": "^3.0.2", 30 | "@typescript-eslint/eslint-plugin": "^5.9.1", 31 | "@typescript-eslint/parser": "^5.9.1", 32 | "@vitejs/plugin-react": "^1.0.7", 33 | "eslint": "^8.7.0", 34 | "eslint-plugin-react": "^7.28.0", 35 | "less": "^4.1.2", 36 | "typescript": "^4.4.4", 37 | "vite": "^2.7.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/App.less: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.less'; 2 | 3 | @primary-color: #3aac59; 4 | @border-radius-base: 20px; 5 | @shadow-color: @primary-color; 6 | 7 | .App { 8 | min-height: 100%; 9 | } 10 | 11 | .container { 12 | width: 1166px; 13 | margin: 0 auto; 14 | } 15 | 16 | .select-theme { 17 | border-radius: 10px; 18 | 19 | input, 20 | .ant-select-selector { 21 | border-radius: 10px !important; 22 | height: 48px !important; 23 | background-color: #f3f5f3 !important; 24 | border: 1px solid #f3f5f3 !important; 25 | } 26 | 27 | .ant-select-selection-item { 28 | line-height: 45px !important; 29 | font-weight: bold !important; 30 | } 31 | } 32 | 33 | .input-theme { 34 | height: 48px; 35 | border-radius: 10px; 36 | background-color: #f3f5f3; 37 | border: 1px solid #f3f5f3; 38 | 39 | .ant-input { 40 | background-color: #f3f5f3; 41 | border-radius: 0; 42 | } 43 | } 44 | 45 | .ant-form-item-label { 46 | font-weight: bold; 47 | } 48 | 49 | .btn-theme { 50 | height: 58px; 51 | line-height: 1; 52 | font-size: 16px; 53 | font-weight: bold; 54 | border-radius: 10px; 55 | opacity: 0.8; 56 | 57 | &.ant-btn-loading { 58 | background-color: #dedede; 59 | border-color: #dedede; 60 | color: #000; 61 | opacity: 1; 62 | } 63 | } 64 | 65 | .btn-theme-color { 66 | background: linear-gradient( 67 | 95.91deg, 68 | #63d17b 4.06%, 69 | #26ddc7 93.65% 70 | ) !important; 71 | border: none; 72 | border-radius: 30px; 73 | text-shadow: none; 74 | 75 | &:hover { 76 | opacity: 0.9; 77 | } 78 | } 79 | 80 | .modal-theme { 81 | .ant-modal-content { 82 | border-radius: 40px; 83 | } 84 | } 85 | 86 | .popover-theme { 87 | .ant-popover-arrow { 88 | display: none; 89 | } 90 | 91 | .ant-popover-inner-content { 92 | margin-top: -5px !important; 93 | } 94 | } 95 | 96 | .modal-theme2 { 97 | .ant-modal-header { 98 | border: none; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd'; 2 | import { useEffect, useState } from 'react'; 3 | import './App.less'; 4 | import bgSvg from './assets/images/bg.svg'; 5 | import svg2 from './assets/images/img-10.svg'; 6 | import svg3 from './assets/images/img-3.svg'; 7 | import svg4 from './assets/images/img-4.svg'; 8 | import svg5 from './assets/images/img-5.svg'; 9 | import svg6 from './assets/images/img-6.svg'; 10 | import svg1 from './assets/images/img-7.svg'; 11 | import logo from './assets/images/full_logo.svg'; 12 | import Donate from './components/Donate'; 13 | import Login from './components/Login'; 14 | import UserInfo from './components/UserInfo'; 15 | 16 | const links = [ 17 | { 18 | icon: svg1, 19 | url: 'https://twitter.com/goby_app' 20 | }, 21 | { 22 | icon: svg2, 23 | url: 'https://discord.gg/rZFf5dugft' 24 | }, 25 | { 26 | icon: svg3, 27 | url: 'https://goby-app.medium.com/' 28 | }, 29 | { 30 | icon: svg5, 31 | url: 'https://github.com/GobyWallet' 32 | }, 33 | { 34 | icon: svg6, 35 | url: 'mailto:dimitry@goby.app' 36 | }, 37 | ] 38 | 39 | declare global { 40 | interface Window { 41 | chia: any; 42 | } 43 | } 44 | 45 | function App() { 46 | const [loginVisible, setLoginVisible] = useState(false) 47 | // const [user, setUser] = useState(false) 48 | const [account, setAccount] = useState(null); 49 | 50 | const handleLogin = () => { 51 | setLoginVisible(false) 52 | // setUser(true) 53 | } 54 | const isGobyInstalled = () => { 55 | const { chia } = window; 56 | return Boolean(chia && chia.isGoby) 57 | } 58 | 59 | const init = async () => { 60 | if (isGobyInstalled()) { 61 | window.chia.on('accountsChanged', (accounts: string[]) => { 62 | setAccount(accounts?.[0]); 63 | }) 64 | window.chia.on('chainChanged', () => window.location.reload()); 65 | 66 | window.chia.request({method: 'accounts'}).then((accounts: string[]) => { 67 | setAccount(accounts?.[0]); 68 | }) 69 | } 70 | } 71 | 72 | const handleConnect = async () => { 73 | if (isGobyInstalled()) { 74 | const accounts = await window.chia.request({method: 'requestAccounts'}); 75 | setAccount(accounts?.[0]); 76 | } else { 77 | setLoginVisible(true); 78 | } 79 | } 80 | 81 | useEffect(() => { 82 | init(); 83 | }, []); 84 | 85 | return ( 86 |
89 |
90 | 91 | 92 | 93 | 94 | {account ? : } 97 |
98 | 99 |
100 | 101 |
102 | 103 |
104 |
105 |
106 | Goby App 107 | @2022 108 |
109 |
110 | {/* Brand assets 111 | FAQ */} 112 | | 113 |
    114 | {links.map((linkItem, index) => 115 |
  • 116 | 117 |
  • ) 118 | } 119 |
120 |
121 |
122 |
123 | 124 | setLoginVisible(false)} /> 125 |
126 | ) 127 | } 128 | 129 | 130 | export default App 131 | -------------------------------------------------------------------------------- /src/api.tsx: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export async function getBalanceByAddress(chainAddress: string): Promise { 4 | const resp = await axios.get("https://api.goby.app/v1/balance", {params: {address: chainAddress}}); 5 | return resp.data.amount; 6 | } -------------------------------------------------------------------------------- /src/assets/images/bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/images/full_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/images/img-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/images/img-10.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/img-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/images/img-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/img-4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/img-5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/img-6.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/img-7.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/img-8.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/img-9.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/chia-util.tsx: -------------------------------------------------------------------------------- 1 | import { bech32m } from 'bech32'; 2 | import { Buffer } from 'buffer'; 3 | import * as clvm from 'clvm'; 4 | import { SHA256 } from 'jscrypto/es6/SHA256'; 5 | import { Hex } from 'jscrypto/es6/Hex'; 6 | import BigNumber from 'bignumber.js'; 7 | 8 | export function sexpTreeHash(pz: clvm.SExp, precalculated: string[] = []): Buffer { 9 | let buf; 10 | const p = pz.pair; 11 | if (p) { 12 | const left = sexpTreeHash(p[0], precalculated); 13 | const right = sexpTreeHash(p[1], precalculated); 14 | buf = Buffer.concat([Buffer.from([2]), left, right]); 15 | } else { 16 | if (precalculated?.find(item => item == Buffer.from(pz.atom!.raw()).toString('hex'))) { 17 | return Buffer.from((pz.atom as any).raw()); 18 | } 19 | buf = Buffer.concat([Buffer.from([1]), (pz.atom as any).raw()]); 20 | } 21 | return Buffer.from(SHA256.hash(Hex.parse(buf.toString('hex'))).toString(Hex), 'hex'); 22 | } 23 | 24 | export function toCatPuzzleHash(assetId: string, innerPuzzleHash: string) { 25 | const pz_hex = ( 26 | 'ff02ffff01ff02ffff01ff02ff5effff04ff02ffff04ffff04ff05ffff04ffff0bff2cff0580ffff04ff0bff80808080ffff04ffff02ff17ff2f80ffff04ff5fffff04ffff02ff2effff04ff02ffff04ff17ff80808080ffff04ffff0bff82027fff82057fff820b7f80ffff04ff81bfffff04ff82017fffff04ff8202ffffff04ff8205ffffff04ff820bffff80808080808080808080808080ffff04ffff01ffffffff81ca3dff46ff0233ffff3c04ff01ff0181cbffffff02ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff22ffff0bff2cff3480ffff0bff22ffff0bff22ffff0bff2cff5c80ff0980ffff0bff22ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ff0bffff01ff02ffff03ffff09ffff02ff2effff04ff02ffff04ff13ff80808080ff820b9f80ffff01ff02ff26ffff04ff02ffff04ffff02ff13ffff04ff5fffff04ff17ffff04ff2fffff04ff81bfffff04ff82017fffff04ff1bff8080808080808080ffff04ff82017fff8080808080ffff01ff088080ff0180ffff01ff02ffff03ff17ffff01ff02ffff03ffff20ff81bf80ffff0182017fffff01ff088080ff0180ffff01ff088080ff018080ff0180ffff04ffff04ff05ff2780ffff04ffff10ff0bff5780ff778080ff02ffff03ff05ffff01ff02ffff03ffff09ffff02ffff03ffff09ff11ff7880ffff0159ff8080ff0180ffff01818f80ffff01ff02ff7affff04ff02ffff04ff0dffff04ff0bffff04ffff04ff81b9ff82017980ff808080808080ffff01ff02ff5affff04ff02ffff04ffff02ffff03ffff09ff11ff7880ffff01ff04ff78ffff04ffff02ff36ffff04ff02ffff04ff13ffff04ff29ffff04ffff0bff2cff5b80ffff04ff2bff80808080808080ff398080ffff01ff02ffff03ffff09ff11ff2480ffff01ff04ff24ffff04ffff0bff20ff2980ff398080ffff010980ff018080ff0180ffff04ffff02ffff03ffff09ff11ff7880ffff0159ff8080ff0180ffff04ffff02ff7affff04ff02ffff04ff0dffff04ff0bffff04ff17ff808080808080ff80808080808080ff0180ffff01ff04ff80ffff04ff80ff17808080ff0180ffffff02ffff03ff05ffff01ff04ff09ffff02ff26ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff0bff22ffff0bff2cff5880ffff0bff22ffff0bff22ffff0bff2cff5c80ff0580ffff0bff22ffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bff2cff058080ff0180ffff04ffff04ff28ffff04ff5fff808080ffff02ff7effff04ff02ffff04ffff04ffff04ff2fff0580ffff04ff5fff82017f8080ffff04ffff02ff7affff04ff02ffff04ff0bffff04ff05ffff01ff808080808080ffff04ff17ffff04ff81bfffff04ff82017fffff04ffff0bff8204ffffff02ff36ffff04ff02ffff04ff09ffff04ff820affffff04ffff0bff2cff2d80ffff04ff15ff80808080808080ff8216ff80ffff04ff8205ffffff04ff820bffff808080808080808080808080ff02ff2affff04ff02ffff04ff5fffff04ff3bffff04ffff02ffff03ff17ffff01ff09ff2dffff0bff27ffff02ff36ffff04ff02ffff04ff29ffff04ff57ffff04ffff0bff2cff81b980ffff04ff59ff80808080808080ff81b78080ff8080ff0180ffff04ff17ffff04ff05ffff04ff8202ffffff04ffff04ffff04ff24ffff04ffff0bff7cff2fff82017f80ff808080ffff04ffff04ff30ffff04ffff0bff81bfffff0bff7cff15ffff10ff82017fffff11ff8202dfff2b80ff8202ff808080ff808080ff138080ff80808080808080808080ff018080ffff04ffff01a072dec062874cd4d3aab892a0906688a1ae412b0109982e1797a170add88bdcdcffff04ffff01a0' + 27 | assetId + 28 | 'ffff04ffff01a0' + 29 | innerPuzzleHash + 30 | 'ff0180808080' 31 | ); 32 | const b = clvm.Bytes.from(pz_hex, 'hex'); 33 | const s = new clvm.Stream(b); 34 | const pz = clvm.sexp_from_stream(new clvm.Stream(b), clvm.SExp.to); 35 | return sexpTreeHash(pz, [innerPuzzleHash]); 36 | } 37 | 38 | export function toChainAddress(address: string | Buffer) { 39 | if (!Buffer.isBuffer(address)) { 40 | address = Buffer.from(address, 'hex'); 41 | } 42 | return bech32m.encode('xch', bech32m.toWords(address)); 43 | } 44 | 45 | export const isValidAddress = (address: string) => { 46 | if (!address) return false; 47 | try { 48 | const data = bech32m.decode(address); 49 | return data && data.words && data.words.length == 52; 50 | } catch (error) { 51 | return false; 52 | } 53 | } 54 | 55 | export function shortenAddress(address = '') { 56 | if (address.length < 11) { 57 | return address; 58 | } 59 | 60 | return `${address.slice(0, 5)}...${address.slice(-4,)}`; 61 | } 62 | 63 | export const splitNumberByStep = ( 64 | num: number | string, 65 | step = 3, 66 | symbol = ',', 67 | forceInt = false 68 | ) => { 69 | // eslint-disable-next-line prefer-const 70 | let [int, float] = (num + '').split('.'); 71 | const reg = new RegExp(`(\\d)(?=(\\d{${step}})+(?!\\d))`, 'g'); 72 | 73 | int = int.replace(reg, `$1${symbol}`); 74 | if (Number(num) > 1000000 || forceInt) { 75 | // hide the after-point part if number is more than 1000000 76 | float = ''; 77 | } 78 | if (float) { 79 | return `${int}.${float}`; 80 | } 81 | return int; 82 | }; 83 | 84 | export function toDecimalAmount(amount: number | BigNumber | string, decimals = 12) { 85 | if (!amount) return '0'; 86 | const bn = new BigNumber(amount).div(Math.pow(10, decimals)); 87 | const str = bn.toFixed(); 88 | const split = str.split('.'); 89 | if (!split[1] || split[1].length < decimals) { 90 | return splitNumberByStep(bn.toFixed()); 91 | } 92 | return splitNumberByStep(bn.toFixed(decimals)); 93 | } -------------------------------------------------------------------------------- /src/components/Donate.less: -------------------------------------------------------------------------------- 1 | .donate-container { 2 | .donate-success { 3 | transition: 0.3s; 4 | transform: scale(0); 5 | opacity: 0; 6 | } 7 | 8 | &.done { 9 | .donate-success { 10 | transform: scale(1); 11 | opacity: 1; 12 | } 13 | .avatar-container { 14 | top: 0 !important; 15 | } 16 | } 17 | } 18 | 19 | .avatar-container { 20 | transition: 0.4s; 21 | &:hover { 22 | > div:nth-child(2) { 23 | transform: translate(55px, -30px) rotate(55deg); 24 | } 25 | } 26 | 27 | > div { 28 | cursor: pointer; 29 | transition: 0.3s; 30 | 31 | &:nth-child(1) { 32 | z-index: 1; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/Donate.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Col, Form, Input, notification, Row } from 'antd' 2 | import BigNumber from 'bignumber.js' 3 | import React, { useEffect, useState } from 'react' 4 | import { getBalanceByAddress } from '../api' 5 | import avatarSvg from '../assets/images/img-1.svg' 6 | import avatar2Svg from '../assets/images/img-2.svg' 7 | import { isValidAddress, toCatPuzzleHash, toChainAddress, toDecimalAmount } from '../chia-util' 8 | import './Donate.less' 9 | import TokenSelect from './TokenSelect' 10 | 11 | const ASSET_LIST = [ 12 | { 13 | symbol: 'XCH', 14 | assetId: '', 15 | decimals: 12, 16 | logo: 'https://static.goby.app/image/token/xch/XCH_32.png', 17 | }, 18 | { 19 | symbol: 'USDS', 20 | assetId: '6d95dae356e32a71db5ddcb42224754a02524c615c5fc35f568c2af04774e589', 21 | decimals: 3, 22 | logo: 'https://static.goby.app/image/token/usds/USDS_32.png' 23 | } 24 | ]; 25 | 26 | type DonateProps = { 27 | account: string | null 28 | } 29 | 30 | 31 | const Donate: React.FC = ({ account }) => { 32 | const [targets, setTargets] = useState([ 33 | { 34 | value: 'xch1r54e052uwuadu2v8cuakayr2ktkr5r7z8t9nnmced08ng6flq6mqrlxgke', 35 | icon: avatarSvg 36 | }, 37 | { 38 | value: 'xch1kj5gvmtl8xc874lkxdnvf9js4c262pk5jdnt42mqcv4nkqqt3m5s2gee6z', 39 | icon: avatar2Svg 40 | } 41 | ]) 42 | const [loading, setLoading] = useState(false) 43 | const [done, setDone] = useState(false) 44 | const [currentAsset, setCurrentAsset] = useState(ASSET_LIST[0]); 45 | const [tokenBalance, setTokenBalance] = useState(null); 46 | const handleSwitchTarget = () => { 47 | const nextTarget = targets.sort(() => -1) 48 | setTargets([...nextTarget]); 49 | const values = form.getFieldsValue(); 50 | form.setFieldsValue({ 51 | ...values, 52 | to: nextTarget[0].value 53 | }) 54 | } 55 | const { useForm } = Form; 56 | const [form] = useForm<{ to: string; amount: string }>(); 57 | const canSubmit = () => { 58 | return account && isValidAddress(form.getFieldValue('to')) && 59 | new BigNumber(form.getFieldValue('amount')).isGreaterThan(0); 60 | } 61 | 62 | const handleFormValuesChange = async (_: any, { to, amount }: { to: string; amount: string }) => { 63 | let resultAmount = amount; 64 | if (!/^\d*(\.\d*)?$/.test(amount)) { 65 | resultAmount = "0"; 66 | } 67 | form.setFieldsValue({ 68 | to, 69 | amount: resultAmount, 70 | }) 71 | } 72 | useEffect(() => { 73 | setTokenBalance(null); 74 | if (!account) { 75 | return; 76 | } 77 | (async () => { 78 | let puzzleHash = account!; 79 | if (currentAsset.assetId) { 80 | puzzleHash = toCatPuzzleHash(currentAsset.assetId, puzzleHash).toString('hex'); 81 | } 82 | const amount = await getBalanceByAddress(toChainAddress(puzzleHash)); 83 | setTokenBalance(amount); 84 | })(); 85 | }, [currentAsset, account]); 86 | 87 | const handleAssetSelect = (asset: any) => { 88 | setCurrentAsset(asset); 89 | const values = form.getFieldsValue(); 90 | form.setFieldsValue({ 91 | ...values, 92 | amount: '' 93 | }) 94 | } 95 | 96 | const handleSubmit = async ({ to, amount }: { to: string, amount: string }) => { 97 | setLoading(true) 98 | const sendAmount = new BigNumber(amount) 99 | .multipliedBy(10 ** currentAsset.decimals) 100 | .toFixed(0); 101 | const params = { 102 | to, 103 | amount: sendAmount, 104 | assetId: currentAsset.assetId, 105 | } 106 | try { 107 | await window.chia.request({ method: 'transfer', params }) 108 | setDone(true) 109 | } catch (error: any) { 110 | console.log(error); 111 | notification.error({ 112 | style: { 113 | width: '340px' 114 | }, 115 | duration: 3, 116 | message: 'Error', 117 | description: error.message, 118 | }); 119 | } 120 | setLoading(false) 121 | } 122 | 123 | return ( 124 |
135 |
137 | {targets.map(token =>
138 | 139 |
)} 140 |
141 | 142 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | {currentAsset.symbol}} /> 173 | 174 | 175 | 176 |
Balance:{tokenBalance == null ? "-": toDecimalAmount(tokenBalance, currentAsset.decimals)}
177 |
178 | 179 | 180 | 183 | 184 | 185 |
186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 |

Sent Successfully

198 |
setDone(false)}> 199 |
200 | Back 201 |
202 |
203 |
204 |
205 | ) 206 | } 207 | 208 | export default Donate 209 | -------------------------------------------------------------------------------- /src/components/Login.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Modal, ModalProps } from "antd"; 2 | import React from "react"; 3 | import "./Donate.less"; 4 | 5 | interface Props extends ModalProps { 6 | onSuccess: () => void; 7 | } 8 | 9 | const Login: React.FC = (props) => { 10 | return ( 11 | 18 |
19 | 27 | 28 | 32 | 38 | 42 | 46 | 47 | 48 |

You haven’t installed any browser wallet

49 |
50 | 63 |
64 |
65 |
66 | ); 67 | }; 68 | 69 | export default Login; 70 | -------------------------------------------------------------------------------- /src/components/TokenSelect.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { Input, Modal } from 'antd' 3 | import React, { useState } from 'react' 4 | import './Donate.less' 5 | 6 | 7 | type TokenSelectProps = { 8 | selectedAsset: any, 9 | tokenList: any[], 10 | onAssetSelected: any, 11 | } 12 | 13 | const TokenSelect: React.FC = ({selectedAsset, tokenList, onAssetSelected}) => { 14 | const [visible, setVisible] = useState(false) 15 | const handleAssetSelect = (asset: any) => { 16 | setVisible(false); 17 | onAssetSelected && onAssetSelected(asset); 18 | } 19 | return ( 20 | <> 21 |
setVisible(true)} className="bg-[#f3f5f3] cursor-pointer justify-between text-[14px] font-bold items-center flex px-[10px] w-full h-[48px] rounded-[10px]"> 22 | 23 | 24 | {selectedAsset.symbol} 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 | 33 | setVisible(false)} visible={visible} className="modal-theme2" width="386px" title="CAT List" footer={false}> 34 | {/*
35 | 36 |
*/} 37 | 38 |
    39 | {tokenList.map(item => 40 |
  • handleAssetSelect(item)} 42 | className="cursor-pointer hover:opacity-80 flex mb-[20px] items-center justify-between"> 43 | 44 | 45 | {item.symbol} 46 | 47 |
  • 48 | )} 49 |
50 |
51 | 52 | ) 53 | } 54 | 55 | export default TokenSelect 56 | -------------------------------------------------------------------------------- /src/components/UserInfo.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Popover } from 'antd' 2 | import { useEffect, useState } from 'react'; 3 | import { getBalanceByAddress } from '../api'; 4 | import { toChainAddress, toDecimalAmount } from '../chia-util'; 5 | 6 | const UserInfo = ({account}: {account: string}) => { 7 | const chainAddress = toChainAddress(account); 8 | const [balance, setBalance] = useState(null); 9 | 10 | useEffect(() => { 11 | (async () => { 12 | const amount = await getBalanceByAddress(chainAddress); 13 | setBalance(amount); 14 | })(); 15 | }, [chainAddress]); 16 | const content =
17 | {chainAddress} 18 |
19 | 20 | return ( 21 | 22 |
23 | {balance == null? "-" : toDecimalAmount(balance)} XCH 24 | 25 | {`${chainAddress.slice(0, 6)}...${chainAddress.slice(-4)}`} 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 |
34 | ) 35 | } 36 | 37 | export default UserInfo 38 | -------------------------------------------------------------------------------- /src/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body { 7 | height: 100%; 8 | padding: 0; 9 | margin: 0; 10 | font-family: 'Poppins', -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, 11 | Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 12 | 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | } 16 | 17 | a { 18 | color: inherit; 19 | text-decoration: none; 20 | } 21 | 22 | a:hover { 23 | opacity: 0.8; 24 | } 25 | 26 | a:active { 27 | opacity: 0.9; 28 | } 29 | 30 | * { 31 | box-sizing: border-box; 32 | } 33 | 34 | #root { 35 | min-height: 100%; 36 | } 37 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom' 2 | import './index.css' 3 | import App from './App' 4 | 5 | ReactDOM.render( 6 | , 7 | document.getElementById('root') 8 | ) 9 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | important: true, 3 | content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": false, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["./src"] 20 | } 21 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | resolve: { 8 | alias: [{ find: /^~/, replacement: '' }], 9 | }, 10 | css: { 11 | preprocessorOptions: { 12 | less: { 13 | javascriptEnabled: true, 14 | }, 15 | }, 16 | }, 17 | server: { 18 | proxy: { 19 | "/v1": { 20 | target: 'https://api.goby.app/', 21 | changeOrigin: true, 22 | secure: false, 23 | } 24 | } 25 | } 26 | }) 27 | --------------------------------------------------------------------------------