├── tsconfig.prod.json ├── .env ├── public ├── favicon.ico ├── manifest.json ├── index.html └── blockies.min.js ├── .prettierrc ├── tsconfig.test.json ├── src ├── assets │ ├── walletconnect-banner.png │ └── algo.svg ├── components │ ├── ASAIcon.tsx │ ├── Banner.tsx │ ├── AccountAssets.tsx │ ├── Icon.tsx │ ├── Blockie.tsx │ ├── Wrapper.tsx │ ├── Column.tsx │ ├── AssetRow.tsx │ ├── Loader.tsx │ ├── Header.tsx │ ├── Button.tsx │ └── Modal.tsx ├── index.tsx ├── helpers │ ├── api.ts │ ├── utilities.ts │ └── types.ts ├── styles.ts ├── scenarios.ts └── App.tsx ├── images.d.ts ├── .gitignore ├── README.md ├── tslint.json ├── .github └── workflows │ └── deploy.yml ├── tsconfig.json ├── .eslintrc ├── package.json └── LICENSE.md /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json" 3 | } -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_VERSION=$npm_package_version 2 | SKIP_PREFLIGHT_CHECK=true 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorand/walletconnect-example-dapp/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "trailingComma": "all", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } -------------------------------------------------------------------------------- /src/assets/walletconnect-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorand/walletconnect-example-dapp/HEAD/src/assets/walletconnect-banner.png -------------------------------------------------------------------------------- /images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' 2 | declare module '*.png' 3 | declare module '*.jpg' 4 | declare module '*.jpeg' 5 | declare module '*.gif' 6 | declare module '*.bmp' 7 | declare module '*.tiff' 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /src/assets/algo.svg: -------------------------------------------------------------------------------- 1 | ALGO_Logos_190320 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WalletConnect V1 Example Dapp 2 | 3 | WalletConnect V1 is being sunset, so it is recommended to look at [WalletConnect](https://github.com/WalletConnect) V2 examples for guidance instead of this repo. 4 | 5 | ## Develop 6 | 7 | ```bash 8 | npm run start 9 | ``` 10 | 11 | ## Test 12 | 13 | ```bash 14 | npm run test 15 | ``` 16 | 17 | ## Build 18 | 19 | ```bash 20 | npm run build 21 | ``` 22 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "WalletConnect Example", 3 | "name": "WalletConnect Example", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/components/ASAIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as PropTypes from "prop-types"; 3 | import Icon from "./Icon"; 4 | 5 | const ASAIcon = (props: { assetID: number }) => { 6 | const src = `https://algoexplorer.io/images/assets/big/light/${props.assetID}.png`; 7 | return ; 8 | }; 9 | 10 | ASAIcon.propTypes = { 11 | assetID: PropTypes.number, 12 | size: PropTypes.number, 13 | }; 14 | 15 | ASAIcon.defaultProps = { 16 | assetID: 0, 17 | size: 20, 18 | }; 19 | 20 | export default ASAIcon; 21 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | import { createGlobalStyle } from "styled-components"; 4 | 5 | import App from "./App"; 6 | import { globalStyle } from "./styles"; 7 | const GlobalStyle = createGlobalStyle` 8 | ${globalStyle} 9 | `; 10 | 11 | declare global { 12 | // tslint:disable-next-line 13 | interface Window { 14 | blockies: any; 15 | } 16 | } 17 | 18 | ReactDOM.render( 19 | <> 20 | 21 | 22 | , 23 | document.getElementById("root"), 24 | ); 25 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"], 3 | "rules": { 4 | "prefer-for-of": false, 5 | "no-shadowed-variable": false, 6 | "no-bitwise": false, 7 | "variable-name": false, 8 | "object-literal-sort-keys": false, 9 | "ordered-imports": false, 10 | "jsx-no-lambda": false, 11 | "jsx-boolean-value": false, 12 | "no-console": false 13 | }, 14 | "linterOptions": { 15 | "exclude": [ 16 | "config/**/*.js", 17 | "node_modules/**/*.ts", 18 | "coverage/lcov-report/*.js" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/Banner.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import styled from "styled-components"; 3 | import banner from "../assets/walletconnect-banner.png"; 4 | 5 | const SBannerWrapper = styled.div` 6 | display: flex; 7 | align-items: center; 8 | position: relative; 9 | `; 10 | 11 | const SBanner = styled.div` 12 | width: 275px; 13 | height: 45px; 14 | background: url(${banner}) no-repeat; 15 | background-size: cover; 16 | background-position: center; 17 | `; 18 | 19 | const Banner = () => ( 20 | 21 | 22 | 23 | ); 24 | 25 | export default Banner; 26 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | WalletConnect Example 11 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup Node 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 14 21 | 22 | - name: Print versions 23 | run: | 24 | node --version 25 | npm --version 26 | 27 | - name: Install and Build 28 | run: | 29 | npm ci 30 | npm run build 31 | 32 | - name: Deploy 33 | uses: JamesIves/github-pages-deploy-action@4.1.4 34 | with: 35 | branch: gh-pages 36 | folder: build 37 | -------------------------------------------------------------------------------- /src/components/AccountAssets.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Column from "./Column"; 3 | import AssetRow from "./AssetRow"; 4 | import { IAssetData } from "../helpers/types"; 5 | 6 | const AccountAssets = (props: { assets: IAssetData[] }) => { 7 | const { assets } = props; 8 | 9 | const nativeCurrency = assets.find((asset: IAssetData) => asset && asset.id === 0) || { 10 | id: 0, 11 | amount: BigInt(0), 12 | creator: "", 13 | frozen: false, 14 | decimals: 6, 15 | name: "Algo", 16 | unitName: "Algo", 17 | }; 18 | 19 | const tokens = assets.filter((asset: IAssetData) => asset && asset.id !== 0); 20 | 21 | return ( 22 | 23 | 24 | {tokens.map(token => ( 25 | 26 | ))} 27 | 28 | ); 29 | }; 30 | 31 | export default AccountAssets; 32 | -------------------------------------------------------------------------------- /src/components/Icon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as PropTypes from "prop-types"; 3 | import styled from "styled-components"; 4 | 5 | interface IIconStyleProps { 6 | size: number; 7 | } 8 | 9 | const SIcon = styled.img` 10 | width: ${({ size }) => `${size}px`}; 11 | height: ${({ size }) => `${size}px`}; 12 | `; 13 | 14 | const Icon = (props: any) => { 15 | const { src, fallback, size } = props; 16 | return ( 17 | { 22 | if (fallback) { 23 | event.target.src = fallback; 24 | } 25 | }} 26 | /> 27 | ); 28 | }; 29 | 30 | Icon.propTypes = { 31 | src: PropTypes.string, 32 | fallback: PropTypes.string, 33 | size: PropTypes.number, 34 | }; 35 | 36 | Icon.defaultProps = { 37 | src: null, 38 | fallback: "", 39 | size: 20, 40 | }; 41 | 42 | export default Icon; 43 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "outDir": "build/dist", 5 | "module": "esnext", 6 | "target": "es5", 7 | "lib": ["es7", "dom"], 8 | "skipLibCheck": true, 9 | "sourceMap": true, 10 | "allowJs": true, 11 | "jsx": "react", 12 | "moduleResolution": "node", 13 | "rootDir": "src", 14 | "forceConsistentCasingInFileNames": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "noImplicitAny": true, 18 | "importHelpers": true, 19 | "strictNullChecks": true, 20 | "allowSyntheticDefaultImports": true, 21 | "suppressImplicitAnyIndexErrors": true, 22 | "noUnusedLocals": true, 23 | "resolveJsonModule": true, 24 | "types": ["react", "jest"] 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "build", 29 | "scripts", 30 | "acceptance-tests", 31 | "webpack", 32 | "jest", 33 | "src/setupTests.ts" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /src/components/Blockie.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import styled from "styled-components"; 3 | 4 | interface IBlockieStyleProps { 5 | size?: number; 6 | } 7 | 8 | interface IBlockieProps extends IBlockieStyleProps { 9 | address: string; 10 | } 11 | 12 | const SBlockieWrapper = styled.div` 13 | width: ${({ size }) => `${size}px`}; 14 | height: ${({ size }) => `${size}px`}; 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | border-radius: 6px; 19 | overflow: hidden; 20 | & img { 21 | width: 100%; 22 | } 23 | `; 24 | 25 | const Blockie = (props: IBlockieProps) => { 26 | const seed = props.address.toLowerCase() || ""; 27 | const imgUrl = window.blockies 28 | .create({ 29 | seed, 30 | }) 31 | .toDataURL(); 32 | return ( 33 | 34 | {props.address} 35 | 36 | ); 37 | }; 38 | 39 | Blockie.defaultProps = { 40 | address: "0x0000000000000000000000000000000000000000", 41 | size: 30, 42 | }; 43 | 44 | export default Blockie; 45 | -------------------------------------------------------------------------------- /src/components/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as PropTypes from "prop-types"; 3 | import styled, { keyframes } from "styled-components"; 4 | 5 | const fadeIn = keyframes` 6 | 0% { 7 | opacity: 0; 8 | } 9 | 100% { 10 | opacity: 1; 11 | } 12 | `; 13 | 14 | interface IWrapperStyleProps { 15 | center: boolean; 16 | } 17 | 18 | const SWrapper = styled.div` 19 | will-change: transform, opacity; 20 | animation: ${fadeIn} 0.7s ease 0s normal 1; 21 | min-height: 200px; 22 | display: flex; 23 | flex-wrap: wrap; 24 | justify-content: center; 25 | align-items: ${({ center }) => (center ? `center` : `flex-start`)}; 26 | `; 27 | 28 | interface IWrapperProps extends IWrapperStyleProps { 29 | children: React.ReactNode; 30 | } 31 | 32 | const Wrapper = (props: IWrapperProps) => { 33 | const { children, center } = props; 34 | return ( 35 | 36 | {children} 37 | 38 | ); 39 | }; 40 | 41 | Wrapper.propTypes = { 42 | children: PropTypes.node.isRequired, 43 | center: PropTypes.bool, 44 | }; 45 | 46 | Wrapper.defaultProps = { 47 | center: false, 48 | }; 49 | 50 | export default Wrapper; 51 | -------------------------------------------------------------------------------- /src/components/Column.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as PropTypes from "prop-types"; 3 | import styled from "styled-components"; 4 | 5 | interface IColumnStyleProps { 6 | spanHeight: boolean; 7 | maxWidth: number; 8 | center: boolean; 9 | } 10 | 11 | interface IColumnProps extends IColumnStyleProps { 12 | children: React.ReactNode; 13 | } 14 | 15 | const SColumn = styled.div` 16 | position: relative; 17 | width: 100%; 18 | height: ${({ spanHeight }) => (spanHeight ? "100%" : "auto")}; 19 | max-width: ${({ maxWidth }) => `${maxWidth}px`}; 20 | margin: 0 auto; 21 | display: flex; 22 | flex-direction: column; 23 | align-items: center; 24 | justify-content: ${({ center }) => (center ? "center" : "flex-start")}; 25 | `; 26 | 27 | const Column = (props: IColumnProps) => { 28 | const { children, spanHeight, maxWidth, center } = props; 29 | return ( 30 | 31 | {children} 32 | 33 | ); 34 | }; 35 | 36 | Column.propTypes = { 37 | children: PropTypes.node.isRequired, 38 | spanHeight: PropTypes.bool, 39 | maxWidth: PropTypes.number, 40 | center: PropTypes.bool, 41 | }; 42 | 43 | Column.defaultProps = { 44 | spanHeight: false, 45 | maxWidth: 600, 46 | center: false, 47 | }; 48 | 49 | export default Column; 50 | -------------------------------------------------------------------------------- /src/components/AssetRow.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import styled from "styled-components"; 3 | import Icon from "./Icon"; 4 | import ASAIcon from "./ASAIcon"; 5 | import algo from "../assets/algo.svg"; 6 | import { formatBigNumWithDecimals } from "../helpers/utilities"; 7 | import { IAssetData } from "../helpers/types"; 8 | 9 | const SAssetRow = styled.div` 10 | width: 100%; 11 | padding: 20px; 12 | display: flex; 13 | justify-content: space-between; 14 | `; 15 | const SAssetRowLeft = styled.div` 16 | display: flex; 17 | `; 18 | const SAssetName = styled.div` 19 | display: flex; 20 | margin-left: 10px; 21 | `; 22 | const SAssetRowRight = styled.div` 23 | display: flex; 24 | `; 25 | const SAssetBalance = styled.div` 26 | display: flex; 27 | `; 28 | 29 | const AssetRow = (props: { asset: IAssetData }) => { 30 | const { asset } = props; 31 | const nativeCurrencyIcon = asset.id === 0 ? algo : null; 32 | return ( 33 | 34 | 35 | {nativeCurrencyIcon ? : } 36 | {asset.name} 37 | 38 | 39 | 40 | {`${formatBigNumWithDecimals(asset.amount, asset.decimals)} ${asset.unitName || "units"}`} 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default AssetRow; 48 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "@typescript-eslint/ban-ts-ignore": ["off"], 4 | "@typescript-eslint/camelcase": ["off"], 5 | "@typescript-eslint/explicit-function-return-type": ["off"], 6 | "@typescript-eslint/interface-name-prefix": ["off"], 7 | "@typescript-eslint/no-explicit-any": ["off"], 8 | "@typescript-eslint/no-unused-expressions": ["off"], 9 | "@typescript-eslint/no-var-requires": ["off"], 10 | "@typescript-eslint/no-use-before-define": ["off"], 11 | "@typescript-eslint/no-unused-vars": ["off"], 12 | "comma-dangle": ["error", "always-multiline"], 13 | "no-async-promise-executor": ["off"], 14 | "no-empty-pattern": ["off"], 15 | "no-undef": ["error"], 16 | "no-var": ["error"], 17 | "object-curly-spacing": ["error", "always"], 18 | "quotes": ["error", "double", { "allowTemplateLiterals": true }], 19 | "semi": ["error", "always"], 20 | "spaced-comment": ["off"], 21 | "no-prototype-builtins": ["off"], 22 | "sort-keys": ["off"], 23 | "space-before-function-paren": ["off"], 24 | "indent": ["off"], 25 | "no-console": ["off"], 26 | "no-useless-catch": ["off"] 27 | }, 28 | "settings": { 29 | "react": { 30 | "pragma": "React", 31 | "version": "detect" 32 | } 33 | }, 34 | "env": { 35 | "browser": true, 36 | "jasmine": true, 37 | "jest": true, 38 | "es6": true 39 | }, 40 | "extends": [ 41 | "standard", 42 | "eslint:recommended", 43 | "plugin:react/recommended", 44 | "plugin:@typescript-eslint/eslint-recommended", 45 | "plugin:@typescript-eslint/recommended", 46 | "prettier/@typescript-eslint", 47 | "plugin:prettier/recommended" 48 | ], 49 | "parser": "@typescript-eslint/parser", 50 | "plugins": ["react", "@typescript-eslint", "prettier"] 51 | } 52 | -------------------------------------------------------------------------------- /src/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as PropTypes from "prop-types"; 3 | import styled, { keyframes } from "styled-components"; 4 | import { colors } from "../styles"; 5 | 6 | const load = keyframes` 7 | 0% { 8 | transform: scale(1.0); 9 | } 10 | 5% { 11 | transform: scale(1.0); 12 | } 13 | 50% { 14 | transform: scale(0.8); 15 | } 16 | 95% { 17 | transform: scale(1.0); 18 | } 19 | 100% { 20 | transform: scale(1.0); 21 | } 22 | `; 23 | 24 | interface ILoaderStyleProps { 25 | size: number; 26 | } 27 | 28 | interface ILoaderProps extends ILoaderStyleProps { 29 | color: string; 30 | } 31 | 32 | const SLoader = styled.svg` 33 | width: ${({ size }) => `${size}px`}; 34 | height: ${({ size }) => `${size}px`}; 35 | animation: ${load} 1s infinite cubic-bezier(0.25, 0, 0.75, 1); 36 | transform: translateZ(0); 37 | `; 38 | 39 | const Loader = (props: ILoaderProps) => { 40 | const { size, color } = props; 41 | const rgb = `rgb(${colors[color]})`; 42 | return ( 43 | 44 | 45 | 51 | 52 | 53 | 54 | ); 55 | }; 56 | 57 | Loader.propTypes = { 58 | size: PropTypes.number, 59 | color: PropTypes.string, 60 | }; 61 | 62 | Loader.defaultProps = { 63 | size: 40, 64 | color: "lightBlue", 65 | }; 66 | 67 | export default Loader; 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "walletconnect-example-dapp", 3 | "version": "1.8.0", 4 | "private": true, 5 | "homepage": "https://algorand.github.io/walletconnect-example-dapp", 6 | "keywords": [ 7 | "walletconnect", 8 | "algorand", 9 | "crypto" 10 | ], 11 | "author": "WalletConnect ", 12 | "license": "LGPL-3.0", 13 | "scripts": { 14 | "start": "react-scripts-ts start", 15 | "build": "react-scripts-ts build", 16 | "test": "react-scripts-ts test --env=jsdom", 17 | "eject": "react-scripts-ts eject", 18 | "lint": "eslint -c './.eslintrc' --fix './src/**/*{.ts,.tsx}'" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/algorand/walletconnect-example-dapp.git" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/algorand/walletconnect-example-dapp/issues" 26 | }, 27 | "dependencies": { 28 | "@json-rpc-tools/utils": "^1.7.5", 29 | "@walletconnect/client": "^1.8.0", 30 | "algorand-walletconnect-qrcode-modal": "^1.8.0", 31 | "algosdk": "^1.11.1", 32 | "blockies-ts": "^1.0.0", 33 | "prop-types": "^15.7.2", 34 | "qr-image": "^3.2.0", 35 | "react": "^16.13.1", 36 | "react-dom": "^16.13.1", 37 | "react-scripts-ts": "^4.0.8", 38 | "styled-components": "^5.2.0" 39 | }, 40 | "devDependencies": { 41 | "@types/jest": "^24.0.25", 42 | "@types/node": "^13.1.6", 43 | "@types/prop-types": "^15.7.3", 44 | "@types/qr-image": "^3.2.3", 45 | "@types/react": "^16.9.51", 46 | "@types/react-dom": "^16.9.8", 47 | "@types/styled-components": "^5.1.3", 48 | "@typescript-eslint/eslint-plugin": "^2.20.0", 49 | "@typescript-eslint/parser": "^2.20.0", 50 | "eslint-config-prettier": "^6.12.0", 51 | "eslint-config-react": "^1.1.7", 52 | "eslint-config-standard": "^14.1.1", 53 | "eslint-plugin-import": "^2.22.1", 54 | "eslint-plugin-node": "^11.1.0", 55 | "eslint-plugin-prettier": "^3.1.4", 56 | "eslint-plugin-promise": "^4.2.1", 57 | "eslint-plugin-standard": "^4.0.1", 58 | "prettier": "^1.19.1", 59 | "typescript": "^4.2.4" 60 | }, 61 | "eslintConfig": { 62 | "extends": "react-app" 63 | }, 64 | "browserslist": [ 65 | ">0.2%", 66 | "not dead", 67 | "not ie <= 11", 68 | "not op_mini all" 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /public/blockies.min.js: -------------------------------------------------------------------------------- 1 | !(function() { 2 | function e(e) { 3 | for (var o = 0; o < c.length; o++) c[o] = 0; 4 | for (var o = 0; o < e.length; o++) 5 | c[o % 4] = (c[o % 4] << 5) - c[o % 4] + e.charCodeAt(o); 6 | } 7 | function o() { 8 | var e = c[0] ^ (c[0] << 11); 9 | return ( 10 | (c[0] = c[1]), 11 | (c[1] = c[2]), 12 | (c[2] = c[3]), 13 | (c[3] = c[3] ^ (c[3] >> 19) ^ e ^ (e >> 8)), 14 | (c[3] >>> 0) / ((1 << 31) >>> 0) 15 | ); 16 | } 17 | function r() { 18 | var e = Math.floor(360 * o()), 19 | r = 60 * o() + 40 + "%", 20 | t = 25 * (o() + o() + o() + o()) + "%", 21 | l = "hsl(" + e + "," + r + "," + t + ")"; 22 | return l; 23 | } 24 | function t(e) { 25 | for ( 26 | var r = e, t = e, l = Math.ceil(r / 2), n = r - l, a = [], c = 0; 27 | t > c; 28 | c++ 29 | ) { 30 | for (var i = [], f = 0; l > f; f++) i[f] = Math.floor(2.3 * o()); 31 | var s = i.slice(0, n); 32 | s.reverse(), (i = i.concat(s)); 33 | for (var h = 0; h < i.length; h++) a.push(i[h]); 34 | } 35 | return a; 36 | } 37 | function l(o) { 38 | var t = {}; 39 | return ( 40 | (t.seed = 41 | o.seed || Math.floor(Math.random() * Math.pow(10, 16)).toString(16)), 42 | e(t.seed), 43 | (t.size = o.size || 8), 44 | (t.scale = o.scale || 4), 45 | (t.color = o.color || r()), 46 | (t.bgcolor = o.bgcolor || r()), 47 | (t.spotcolor = o.spotcolor || r()), 48 | t 49 | ); 50 | } 51 | function n(e, o) { 52 | var r = t(e.size), 53 | l = Math.sqrt(r.length); 54 | o.width = o.height = e.size * e.scale; 55 | var n = o.getContext("2d"); 56 | (n.fillStyle = e.bgcolor), 57 | n.fillRect(0, 0, o.width, o.height), 58 | (n.fillStyle = e.color); 59 | for (var a = 0; a < r.length; a++) 60 | if (r[a]) { 61 | var c = Math.floor(a / l), 62 | i = a % l; 63 | (n.fillStyle = 1 == r[a] ? e.color : e.spotcolor), 64 | n.fillRect(i * e.scale, c * e.scale, e.scale, e.scale); 65 | } 66 | return o; 67 | } 68 | function a(e) { 69 | var e = l(e || {}), 70 | o = document.createElement("canvas"); 71 | return n(e, o), o; 72 | } 73 | var c = new Array(4), 74 | i = { create: a, render: n }; 75 | "undefined" != typeof module && (module.exports = i), 76 | "undefined" != typeof window && (window.blockies = i); 77 | })(); 78 | -------------------------------------------------------------------------------- /src/helpers/api.ts: -------------------------------------------------------------------------------- 1 | import algosdk from "algosdk"; 2 | import { IAssetData } from "./types"; 3 | 4 | export enum ChainType { 5 | MainNet = "mainnet", 6 | TestNet = "testnet", 7 | } 8 | 9 | const mainNetClient = new algosdk.Algodv2("", "https://mainnet-api.algonode.cloud", ""); 10 | const testNetClient = new algosdk.Algodv2("", "https://testnet-api.algonode.cloud", ""); 11 | 12 | function clientForChain(chain: ChainType): algosdk.Algodv2 { 13 | switch (chain) { 14 | case ChainType.MainNet: 15 | return mainNetClient; 16 | case ChainType.TestNet: 17 | return testNetClient; 18 | default: 19 | throw new Error(`Unknown chain type: ${chain}`); 20 | } 21 | } 22 | 23 | export async function apiGetAccountAssets( 24 | chain: ChainType, 25 | address: string, 26 | ): Promise { 27 | const client = clientForChain(chain); 28 | 29 | const accountInfo = await client 30 | .accountInformation(address) 31 | .setIntDecoding(algosdk.IntDecoding.BIGINT) 32 | .do(); 33 | 34 | const algoBalance = accountInfo.amount as bigint; 35 | const assetsFromRes: Array<{ 36 | "asset-id": bigint; 37 | amount: bigint; 38 | creator: string; 39 | frozen: boolean; 40 | }> = accountInfo.assets; 41 | 42 | const assets: IAssetData[] = assetsFromRes.map(({ "asset-id": id, amount, creator, frozen }) => ({ 43 | id: Number(id), 44 | amount, 45 | creator, 46 | frozen, 47 | decimals: 0, 48 | })); 49 | 50 | assets.sort((a, b) => a.id - b.id); 51 | 52 | await Promise.all( 53 | assets.map(async asset => { 54 | const { params } = await client.getAssetByID(asset.id).do(); 55 | asset.name = params.name; 56 | asset.unitName = params["unit-name"]; 57 | asset.url = params.url; 58 | asset.decimals = params.decimals; 59 | }), 60 | ); 61 | 62 | assets.unshift({ 63 | id: 0, 64 | amount: algoBalance, 65 | creator: "", 66 | frozen: false, 67 | decimals: 6, 68 | name: "Algo", 69 | unitName: "Algo", 70 | }); 71 | 72 | return assets; 73 | } 74 | 75 | export async function apiGetTxnParams(chain: ChainType): Promise { 76 | const params = await clientForChain(chain) 77 | .getTransactionParams() 78 | .do(); 79 | return params; 80 | } 81 | 82 | export async function apiSubmitTransactions( 83 | chain: ChainType, 84 | stxns: Uint8Array[], 85 | ): Promise { 86 | const { txId } = await clientForChain(chain) 87 | .sendRawTransaction(stxns) 88 | .do(); 89 | return await waitForTransaction(chain, txId); 90 | } 91 | 92 | async function waitForTransaction(chain: ChainType, txId: string): Promise { 93 | const client = clientForChain(chain); 94 | 95 | let lastStatus = await client.status().do(); 96 | let lastRound = lastStatus["last-round"]; 97 | while (true) { 98 | const status = await client.pendingTransactionInformation(txId).do(); 99 | if (status["pool-error"]) { 100 | throw new Error(`Transaction Pool Error: ${status["pool-error"]}`); 101 | } 102 | if (status["confirmed-round"]) { 103 | return status["confirmed-round"]; 104 | } 105 | lastStatus = await client.statusAfterBlock(lastRound + 1).do(); 106 | lastRound = lastStatus["last-round"]; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import styled from "styled-components"; 3 | import * as PropTypes from "prop-types"; 4 | import Blockie from "./Blockie"; 5 | import { ellipseAddress } from "../helpers/utilities"; 6 | import { transitions } from "../styles"; 7 | import { ChainType } from "src/helpers/api"; 8 | 9 | const SHeader = styled.div` 10 | margin-top: -1px; 11 | margin-bottom: 1px; 12 | width: 100%; 13 | height: 100px; 14 | display: flex; 15 | align-items: center; 16 | justify-content: space-between; 17 | padding: 0 16px; 18 | `; 19 | 20 | const SActiveAccount = styled.div` 21 | display: flex; 22 | align-items: center; 23 | position: relative; 24 | font-weight: 500; 25 | `; 26 | 27 | const SActiveChain = styled(SActiveAccount as any)` 28 | flex-direction: column; 29 | text-align: left; 30 | align-items: flex-start; 31 | & p { 32 | font-size: 0.8em; 33 | margin: 0; 34 | padding: 0; 35 | } 36 | & p:nth-child(2) { 37 | font-weight: bold; 38 | } 39 | `; 40 | 41 | const SBlockie = styled(Blockie as any)` 42 | margin-right: 10px; 43 | `; 44 | 45 | interface IHeaderStyle { 46 | connected: boolean; 47 | } 48 | 49 | const SAddress = styled.p` 50 | transition: ${transitions.base}; 51 | font-weight: bold; 52 | margin: ${({ connected }) => (connected ? "-2px auto 0.7em" : "0")}; 53 | `; 54 | 55 | const SDisconnect = styled.div` 56 | transition: ${transitions.button}; 57 | font-size: 12px; 58 | font-family: monospace; 59 | position: absolute; 60 | right: 0; 61 | top: 20px; 62 | opacity: 0.7; 63 | cursor: pointer; 64 | 65 | opacity: ${({ connected }) => (connected ? 1 : 0)}; 66 | visibility: ${({ connected }) => (connected ? "visible" : "hidden")}; 67 | pointer-events: ${({ connected }) => (connected ? "auto" : "none")}; 68 | 69 | &:hover { 70 | transform: translateY(-1px); 71 | opacity: 0.5; 72 | } 73 | `; 74 | 75 | interface IHeaderProps { 76 | killSession: () => unknown; 77 | connected: boolean; 78 | address: string; 79 | chain: ChainType; 80 | chainUpdate: (newChain: ChainType) => unknown; 81 | } 82 | 83 | function stringToChainType(s: string): ChainType { 84 | switch (s) { 85 | case ChainType.MainNet.toString(): 86 | return ChainType.MainNet; 87 | case ChainType.TestNet.toString(): 88 | return ChainType.TestNet; 89 | default: 90 | throw new Error(`Unknown chain selected: ${s}`); 91 | } 92 | } 93 | 94 | const Header = (props: IHeaderProps) => { 95 | const { connected, address, killSession } = props; 96 | return ( 97 | 98 | {connected && ( 99 | 100 |

101 | {`Connected to `} 102 | 109 |

110 |
111 | )} 112 | {address && ( 113 | 114 | 115 | {ellipseAddress(address)} 116 | 117 | {"Disconnect"} 118 | 119 | 120 | )} 121 |
122 | ); 123 | }; 124 | 125 | Header.propTypes = { 126 | killSession: PropTypes.func.isRequired, 127 | address: PropTypes.string, 128 | }; 129 | 130 | export default Header; 131 | -------------------------------------------------------------------------------- /src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import styled from "styled-components"; 3 | import Loader from "./Loader"; 4 | import { colors, fonts, shadows, transitions } from "../styles"; 5 | 6 | interface IButtonStyleProps { 7 | fetching: boolean; 8 | outline: boolean; 9 | type: "button" | "submit" | "reset"; 10 | color: string; 11 | disabled: boolean; 12 | icon: any; 13 | left: boolean; 14 | } 15 | 16 | interface IButtonProps extends IButtonStyleProps { 17 | children: React.ReactNode; 18 | onClick?: any; 19 | } 20 | 21 | const SIcon = styled.div` 22 | position: absolute; 23 | height: 15px; 24 | width: 15px; 25 | margin: 0 8px; 26 | top: calc((100% - 15px) / 2); 27 | `; 28 | 29 | const SHoverLayer = styled.div` 30 | transition: ${transitions.button}; 31 | position: absolute; 32 | height: 100%; 33 | width: 100%; 34 | background-color: rgb(${colors.white}, 0.1); 35 | top: 0; 36 | bottom: 0; 37 | right: 0; 38 | left: 0; 39 | pointer-events: none; 40 | opacity: 0; 41 | visibility: hidden; 42 | `; 43 | 44 | const SButton = styled.button` 45 | transition: ${transitions.button}; 46 | position: relative; 47 | border: none; 48 | border-style: none; 49 | box-sizing: border-box; 50 | background-color: ${({ outline, color }) => (outline ? "transparent" : `rgb(${colors[color]})`)}; 51 | border: ${({ outline, color }) => (outline ? `1px solid rgb(${colors[color]})` : "none")}; 52 | color: ${({ outline, color }) => (outline ? `rgb(${colors[color]})` : `rgb(${colors.white})`)}; 53 | box-shadow: ${({ outline }) => (outline ? "none" : `${shadows.soft}`)}; 54 | border-radius: 8px; 55 | font-size: ${fonts.size.medium}; 56 | font-weight: ${fonts.weight.semibold}; 57 | padding: ${({ icon, left }) => 58 | icon ? (left ? "7px 12px 8px 28px" : "7px 28px 8px 12px") : "8px 12px"}; 59 | cursor: ${({ disabled }) => (disabled ? "auto" : "pointer")}; 60 | will-change: transform; 61 | 62 | &:disabled { 63 | opacity: 0.6; 64 | box-shadow: ${({ outline }) => (outline ? "none" : `${shadows.soft}`)}; 65 | } 66 | 67 | @media (hover: hover) { 68 | &:hover { 69 | transform: ${({ disabled }) => (!disabled ? "translateY(-1px)" : "none")}; 70 | box-shadow: ${({ disabled, outline }) => 71 | !disabled ? (outline ? "none" : `${shadows.hover}`) : `${shadows.soft}`}; 72 | } 73 | 74 | &:hover ${SHoverLayer} { 75 | opacity: 1; 76 | visibility: visible; 77 | } 78 | } 79 | 80 | &:active { 81 | transform: ${({ disabled }) => (!disabled ? "translateY(1px)" : "none")}; 82 | box-shadow: ${({ outline }) => (outline ? "none" : `${shadows.soft}`)}; 83 | color: ${({ outline, color }) => 84 | outline ? `rgb(${colors[color]})` : `rgba(${colors.white}, 0.24)`}; 85 | 86 | & ${SIcon} { 87 | opacity: 0.8; 88 | } 89 | } 90 | 91 | & ${SIcon} { 92 | right: ${({ left }) => (left ? "auto" : "0")}; 93 | left: ${({ left }) => (left ? "0" : "auto")}; 94 | display: ${({ icon }) => (icon ? "block" : "none")}; 95 | mask: ${({ icon }) => (icon ? `url(${icon}) center no-repeat` : "none")}; 96 | background-color: ${({ outline, color }) => 97 | outline ? `rgb(${colors[color]})` : `rgb(${colors.white})`}; 98 | transition: 0.15s ease; 99 | } 100 | `; 101 | 102 | const Button = (props: IButtonProps) => ( 103 | 112 | 113 | 114 | {props.fetching ? : props.children} 115 | 116 | ); 117 | 118 | Button.defaultProps = { 119 | fetching: false, 120 | outline: false, 121 | type: "button", 122 | color: "lightBlue", 123 | disabled: false, 124 | icon: null, 125 | left: false, 126 | }; 127 | 128 | export default Button; 129 | -------------------------------------------------------------------------------- /src/helpers/utilities.ts: -------------------------------------------------------------------------------- 1 | export function capitalize(string: string): string { 2 | return string 3 | .split(" ") 4 | .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) 5 | .join(" "); 6 | } 7 | 8 | export function ellipseText(text = "", maxLength = 9999): string { 9 | if (text.length <= maxLength) { 10 | return text; 11 | } 12 | const _maxLength = maxLength - 3; 13 | let ellipse = false; 14 | let currentLength = 0; 15 | const result = 16 | text 17 | .split(" ") 18 | .filter(word => { 19 | currentLength += word.length; 20 | if (ellipse || currentLength >= _maxLength) { 21 | ellipse = true; 22 | return false; 23 | } else { 24 | return true; 25 | } 26 | }) 27 | .join(" ") + "..."; 28 | return result; 29 | } 30 | 31 | export function ellipseAddress(address = "", width = 6): string { 32 | return `${address.slice(0, width)}...${address.slice(-width)}`; 33 | } 34 | 35 | export function padLeft(n: string, width: number, z?: string): string { 36 | z = z || "0"; 37 | n = n + ""; 38 | return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; 39 | } 40 | 41 | export function sanitizeHex(hex: string): string { 42 | hex = hex.substring(0, 2) === "0x" ? hex.substring(2) : hex; 43 | if (hex === "") { 44 | return ""; 45 | } 46 | hex = hex.length % 2 !== 0 ? "0" + hex : hex; 47 | return "0x" + hex; 48 | } 49 | 50 | export function removeHexPrefix(hex: string): string { 51 | return hex.toLowerCase().replace("0x", ""); 52 | } 53 | 54 | export function getDataString(func: string, arrVals: any[]): string { 55 | let val = ""; 56 | for (let i = 0; i < arrVals.length; i++) { 57 | val += padLeft(arrVals[i], 64); 58 | } 59 | const data = func + val; 60 | return data; 61 | } 62 | 63 | export function isMobile(): boolean { 64 | let mobile = false; 65 | 66 | function hasTouchEvent(): boolean { 67 | try { 68 | document.createEvent("TouchEvent"); 69 | return true; 70 | } catch (e) { 71 | return false; 72 | } 73 | } 74 | 75 | function hasMobileUserAgent(): boolean { 76 | if ( 77 | /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test( 78 | navigator.userAgent, 79 | ) || 80 | /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test( 81 | navigator.userAgent.substr(0, 4), 82 | ) 83 | ) { 84 | return true; 85 | } else if (hasTouchEvent()) { 86 | return true; 87 | } 88 | return false; 89 | } 90 | 91 | mobile = hasMobileUserAgent(); 92 | 93 | return mobile; 94 | } 95 | 96 | export function formatBigNumWithDecimals(num: bigint, decimals: number): string { 97 | const singleUnit = BigInt("1" + "0".repeat(decimals)); 98 | const wholeUnits = num / singleUnit; 99 | const fractionalUnits = num % singleUnit; 100 | 101 | return wholeUnits.toString() + "." + fractionalUnits.toString().padStart(decimals, "0"); 102 | } 103 | -------------------------------------------------------------------------------- /src/helpers/types.ts: -------------------------------------------------------------------------------- 1 | export interface IAssetData { 2 | id: number; 3 | amount: bigint; 4 | creator: string; 5 | frozen: boolean; 6 | decimals: number; 7 | name?: string; 8 | unitName?: string; 9 | url?: string; 10 | } 11 | 12 | export interface IChainData { 13 | name: string; 14 | short_name: string; 15 | chain: string; 16 | network: string; 17 | chain_id: number; 18 | network_id: number; 19 | rpc_url: string; 20 | native_currency: IAssetData; 21 | } 22 | export interface ITxData { 23 | from: string; 24 | to: string; 25 | nonce: string; 26 | gasPrice: string; 27 | gasLimit: string; 28 | value: string; 29 | data: string; 30 | } 31 | 32 | export interface IBlockScoutTx { 33 | value: string; 34 | txreceipt_status: string; 35 | transactionIndex: string; 36 | to: string; 37 | timeStamp: string; 38 | nonce: string; 39 | isError: string; 40 | input: string; 41 | hash: string; 42 | gasUsed: string; 43 | gasPrice: string; 44 | gas: string; 45 | from: string; 46 | cumulativeGasUsed: string; 47 | contractAddress: string; 48 | confirmations: string; 49 | blockNumber: string; 50 | blockHash: string; 51 | } 52 | 53 | export interface IBlockScoutTokenTx { 54 | value: string; 55 | transactionIndex: string; 56 | tokenSymbol: string; 57 | tokenName: string; 58 | tokenDecimal: string; 59 | to: string; 60 | timeStamp: string; 61 | nonce: string; 62 | input: string; 63 | hash: string; 64 | gasUsed: string; 65 | gasPrice: string; 66 | gas: string; 67 | from: string; 68 | cumulativeGasUsed: string; 69 | contractAddress: string; 70 | confirmations: string; 71 | blockNumber: string; 72 | blockHash: string; 73 | } 74 | 75 | export interface IParsedTx { 76 | timestamp: string; 77 | hash: string; 78 | from: string; 79 | to: string; 80 | nonce: string; 81 | gasPrice: string; 82 | gasUsed: string; 83 | fee: string; 84 | value: string; 85 | input: string; 86 | error: boolean; 87 | asset: IAssetData; 88 | operations: ITxOperation[]; 89 | } 90 | 91 | export interface ITxOperation { 92 | asset: IAssetData; 93 | value: string; 94 | from: string; 95 | to: string; 96 | functionName: string; 97 | } 98 | 99 | export interface IGasPricesResponse { 100 | fastWait: number; 101 | avgWait: number; 102 | blockNum: number; 103 | fast: number; 104 | fastest: number; 105 | fastestWait: number; 106 | safeLow: number; 107 | safeLowWait: number; 108 | speed: number; 109 | block_time: number; 110 | average: number; 111 | } 112 | 113 | export interface IGasPrice { 114 | time: number; 115 | price: number; 116 | } 117 | 118 | export interface IGasPrices { 119 | timestamp: number; 120 | slow: IGasPrice; 121 | average: IGasPrice; 122 | fast: IGasPrice; 123 | } 124 | 125 | export interface IMethodArgument { 126 | type: string; 127 | } 128 | 129 | export interface IMethod { 130 | signature: string; 131 | name: string; 132 | args: IMethodArgument[]; 133 | } 134 | 135 | /* eslint-disable */ 136 | 137 | /** 138 | * Options for creating and using a multisignature account. 139 | */ 140 | export interface IMultisigMetadata { 141 | /** 142 | * Multisig version. 143 | */ 144 | version: number; 145 | 146 | /** 147 | * Multisig threshold value. Authorization requires a subset of 148 | * signatures, equal to or greater than the threshold value. 149 | */ 150 | threshold: number; 151 | 152 | /** 153 | * List of Algorand addresses of possible signers for this 154 | * multisig. Order is important. 155 | */ 156 | addrs: string[]; 157 | } 158 | 159 | export interface IWalletTransaction { 160 | /** 161 | * Base64 encoding of the canonical msgpack encoding of a 162 | * Transaction. 163 | */ 164 | txn: string; 165 | 166 | /** 167 | * Optional authorized address used to sign the transaction when 168 | * the account is rekeyed. Also called the signor/sgnr. 169 | */ 170 | authAddr?: string; 171 | 172 | /** 173 | * Optional multisig metadata used to sign the transaction 174 | */ 175 | msig?: IMultisigMetadata; 176 | 177 | /** 178 | * Optional list of addresses that must sign the transactions 179 | */ 180 | signers?: string[]; 181 | 182 | /** 183 | * Optional message explaining the reason of the transaction 184 | */ 185 | message?: string; 186 | } 187 | 188 | export interface ISignTxnOpts { 189 | /** 190 | * Optional message explaining the reason of the group of 191 | * transactions. 192 | */ 193 | message?: string; 194 | 195 | // other options may be present, but are not standard 196 | } 197 | 198 | export type SignTxnParams = [IWalletTransaction[], ISignTxnOpts?]; 199 | 200 | /* eslint-enable */ 201 | -------------------------------------------------------------------------------- /src/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as PropTypes from "prop-types"; 3 | import styled from "styled-components"; 4 | import { colors, transitions } from "../styles"; 5 | 6 | interface ILightboxStyleProps { 7 | show: boolean; 8 | offset: number; 9 | opacity?: number; 10 | } 11 | 12 | const SLightbox = styled.div` 13 | transition: opacity 0.1s ease-in-out; 14 | text-align: center; 15 | position: absolute; 16 | width: 100vw; 17 | height: 100vh; 18 | margin-left: -50vw; 19 | top: ${({ offset }) => (offset ? `-${offset}px` : 0)}; 20 | left: 50%; 21 | z-index: 2; 22 | will-change: opacity; 23 | background-color: ${({ opacity }) => { 24 | let alpha = 0.4; 25 | if (typeof opacity === "number") { 26 | alpha = opacity; 27 | } 28 | return `rgba(0, 0, 0, ${alpha})`; 29 | }}; 30 | opacity: ${({ show }) => (show ? 1 : 0)}; 31 | visibility: ${({ show }) => (show ? "visible" : "hidden")}; 32 | pointer-events: ${({ show }) => (show ? "auto" : "none")}; 33 | display: flex; 34 | justify-content: center; 35 | align-items: center; 36 | `; 37 | 38 | const SModalContainer = styled.div` 39 | position: relative; 40 | width: 100%; 41 | height: 100%; 42 | padding: 15px; 43 | display: flex; 44 | align-items: center; 45 | justify-content: center; 46 | `; 47 | 48 | const SHitbox = styled.div` 49 | position: absolute; 50 | top: 0; 51 | left: 0; 52 | right: 0; 53 | bottom: 0; 54 | `; 55 | 56 | interface ICloseButtonStyleProps { 57 | size: number; 58 | color: string; 59 | onClick?: any; 60 | } 61 | 62 | const SCloseButton = styled.div` 63 | transition: ${transitions.short}; 64 | position: absolute; 65 | width: ${({ size }) => `${size}px`}; 66 | height: ${({ size }) => `${size}px`}; 67 | right: ${({ size }) => `${size / 1.6667}px`}; 68 | top: ${({ size }) => `${size / 1.6667}px`}; 69 | opacity: 0.5; 70 | cursor: pointer; 71 | &:hover { 72 | opacity: 1; 73 | } 74 | &:before, 75 | &:after { 76 | position: absolute; 77 | content: " "; 78 | height: ${({ size }) => `${size}px`}; 79 | width: 2px; 80 | background: ${({ color }) => `rgb(${colors[color]})`}; 81 | } 82 | &:before { 83 | transform: rotate(45deg); 84 | } 85 | &:after { 86 | transform: rotate(-45deg); 87 | } 88 | `; 89 | 90 | const SCard = styled.div` 91 | position: relative; 92 | width: 100%; 93 | max-width: 500px; 94 | padding: 25px; 95 | background-color: rgb(${colors.white}); 96 | border-radius: 6px; 97 | display: flex; 98 | flex-direction: column; 99 | justify-content: center; 100 | align-items: center; 101 | `; 102 | 103 | const SModalContent = styled.div` 104 | position: relative; 105 | width: 100%; 106 | position: relative; 107 | word-wrap: break-word; 108 | `; 109 | 110 | interface IModalState { 111 | offset: number; 112 | } 113 | 114 | interface IModalProps { 115 | children: React.ReactNode; 116 | show: boolean; 117 | toggleModal: any; 118 | opacity?: number; 119 | } 120 | 121 | const INITIAL_STATE: IModalState = { 122 | offset: 0, 123 | }; 124 | 125 | class Modal extends React.Component { 126 | public static propTypes = { 127 | children: PropTypes.node.isRequired, 128 | show: PropTypes.bool.isRequired, 129 | toggleModal: PropTypes.func.isRequired, 130 | opacity: PropTypes.number, 131 | }; 132 | 133 | public lightbox?: HTMLDivElement | null; 134 | 135 | public state: IModalState = { 136 | ...INITIAL_STATE, 137 | }; 138 | 139 | public componentDidUpdate() { 140 | if (this.lightbox) { 141 | const lightboxRect = this.lightbox.getBoundingClientRect(); 142 | const offset = lightboxRect.top > 0 ? lightboxRect.top : 0; 143 | 144 | if (offset !== INITIAL_STATE.offset && offset !== this.state.offset) { 145 | this.setState({ offset }); 146 | } 147 | } 148 | } 149 | 150 | public toggleModal = async () => { 151 | const d = typeof window !== "undefined" ? document : ""; 152 | const body = d ? d.body || d.getElementsByTagName("body")[0] : ""; 153 | if (body) { 154 | if (this.props.show) { 155 | body.style.position = ""; 156 | } else { 157 | body.style.position = "fixed"; 158 | } 159 | } 160 | this.props.toggleModal(); 161 | }; 162 | 163 | public render() { 164 | const { offset } = this.state; 165 | const { children, show, opacity } = this.props; 166 | return ( 167 | (this.lightbox = c)}> 168 | 169 | 170 | 171 | 172 | 173 | {children} 174 | 175 | 176 | 177 | ); 178 | } 179 | } 180 | 181 | export default Modal; 182 | -------------------------------------------------------------------------------- /src/styles.ts: -------------------------------------------------------------------------------- 1 | export const colors = { 2 | white: "255, 255, 255", 3 | black: "0, 0, 0", 4 | dark: "12, 12, 13", 5 | grey: "169, 169, 188", 6 | darkGrey: "113, 119, 138", 7 | lightGrey: "212, 212, 212", 8 | blue: "101, 127, 230", 9 | lightBlue: "64, 153, 255", 10 | yellow: "250, 188, 45", 11 | orange: "246, 133, 27", 12 | green: "84, 209, 146", 13 | pink: "255, 51, 102", 14 | red: "214, 75, 71", 15 | purple: "110, 107, 233", 16 | }; 17 | 18 | export const fonts = { 19 | size: { 20 | tiny: "10px", 21 | small: "14px", 22 | medium: "16px", 23 | large: "18px", 24 | h1: "60px", 25 | h2: "50px", 26 | h3: "40px", 27 | h4: "32px", 28 | h5: "24px", 29 | h6: "20px", 30 | }, 31 | weight: { 32 | normal: 400, 33 | medium: 500, 34 | semibold: 600, 35 | bold: 700, 36 | extrabold: 800, 37 | }, 38 | family: { 39 | OpenSans: `"Open Sans", sans-serif`, 40 | }, 41 | }; 42 | 43 | export const transitions = { 44 | short: "all 0.1s ease-in-out", 45 | base: "all 0.2s ease-in-out", 46 | long: "all 0.3s ease-in-out", 47 | button: "all 0.15s ease-in-out", 48 | }; 49 | 50 | export const shadows = { 51 | soft: 52 | "0 4px 6px 0 rgba(50, 50, 93, 0.11), 0 1px 3px 0 rgba(0, 0, 0, 0.08), inset 0 0 1px 0 rgba(0, 0, 0, 0.06)", 53 | medium: 54 | "0 3px 6px 0 rgba(0, 0, 0, 0.06), 0 0 1px 0 rgba(50, 50, 93, 0.02), 0 5px 10px 0 rgba(59, 59, 92, 0.08)", 55 | big: "0 15px 35px 0 rgba(50, 50, 93, 0.06), 0 5px 15px 0 rgba(50, 50, 93, 0.15)", 56 | hover: 57 | "0 7px 14px 0 rgba(50, 50, 93, 0.1), 0 3px 6px 0 rgba(0, 0, 0, 0.08), inset 0 0 1px 0 rgba(0, 0, 0, 0.06)", 58 | }; 59 | 60 | export const responsive = { 61 | xs: { 62 | min: "min-width: 467px", 63 | max: "max-width: 468px", 64 | }, 65 | sm: { 66 | min: "min-width: 639px", 67 | max: "max-width: 640px", 68 | }, 69 | md: { 70 | min: "min-width: 959px", 71 | max: "max-width: 960px", 72 | }, 73 | lg: { 74 | min: "min-width: 1023px", 75 | max: "max-width: 1024px", 76 | }, 77 | xl: { 78 | min: "min-width: 1399px", 79 | max: "max-width: 1400px", 80 | }, 81 | }; 82 | 83 | export const globalStyle = ` 84 | 85 | html, body, #root { 86 | height: 100%; 87 | width: 100%; 88 | margin: 0; 89 | padding: 0; 90 | } 91 | 92 | body { 93 | font-family: ${fonts.family.OpenSans}; 94 | font-style: normal; 95 | font-stretch: normal; 96 | font-weight: ${fonts.weight.normal}; 97 | font-size: ${fonts.size.medium}; 98 | background-color: rgb(${colors.white}); 99 | color: rgb(${colors.dark}); 100 | overflow-y:auto; 101 | text-rendering: optimizeLegibility; 102 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 103 | -webkit-font-smoothing: antialiased; 104 | -moz-osx-font-smoothing: grayscale; 105 | -webkit-text-size-adjust: 100%; 106 | -webkit-overflow-scrolling: touch; 107 | -ms-text-size-adjust: 100%; 108 | -webkit-text-size-adjust: 100%; 109 | } 110 | 111 | button { 112 | border-style: none; 113 | line-height: 1em; 114 | background-image: none; 115 | outline: 0; 116 | -webkit-box-shadow: none; 117 | box-shadow: none; 118 | } 119 | 120 | [tabindex] { 121 | outline: none; 122 | width: 100%; 123 | height: 100%; 124 | } 125 | 126 | a, p, h1, h2, h3, h4, h5, h6 { 127 | text-decoration: none; 128 | margin: 0; 129 | padding: 0; 130 | margin: 0.7em 0; 131 | } 132 | 133 | h1 { 134 | font-size: ${fonts.size.h1} 135 | } 136 | h2 { 137 | font-size: ${fonts.size.h2} 138 | } 139 | h3 { 140 | font-size: ${fonts.size.h3} 141 | } 142 | h4 { 143 | font-size: ${fonts.size.h4} 144 | } 145 | h5 { 146 | font-size: ${fonts.size.h5} 147 | } 148 | h6 { 149 | font-size: ${fonts.size.h6} 150 | } 151 | 152 | a { 153 | background-color: transparent; 154 | -webkit-text-decoration-skip: objects; 155 | text-decoration: none; 156 | color: inherit; 157 | outline: none; 158 | } 159 | 160 | b, 161 | strong { 162 | font-weight: inherit; 163 | font-weight: bolder; 164 | } 165 | 166 | ul, li { 167 | list-style: none; 168 | margin: 0; 169 | padding: 0; 170 | } 171 | 172 | * { 173 | box-sizing: border-box !important; 174 | } 175 | 176 | 177 | input { 178 | -webkit-appearance: none; 179 | } 180 | 181 | article, 182 | aside, 183 | details, 184 | figcaption, 185 | figure, 186 | footer, 187 | header, 188 | main, 189 | menu, 190 | nav, 191 | section, 192 | summary { 193 | display: block; 194 | } 195 | audio, 196 | canvas, 197 | progress, 198 | video { 199 | display: inline-block; 200 | } 201 | 202 | input[type="color"], 203 | input[type="date"], 204 | input[type="datetime"], 205 | input[type="datetime-local"], 206 | input[type="email"], 207 | input[type="month"], 208 | input[type="number"], 209 | input[type="password"], 210 | input[type="search"], 211 | input[type="tel"], 212 | input[type="text"], 213 | input[type="time"], 214 | input[type="url"], 215 | input[type="week"], 216 | select:focus, 217 | textarea { 218 | font-size: 16px; 219 | } 220 | `; 221 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2018 WalletConnect Association. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | This version of the GNU Lesser General Public License incorporates 9 | the terms and conditions of version 3 of the GNU General Public 10 | License, supplemented by the additional permissions listed below. 11 | 12 | 0. Additional Definitions. 13 | 14 | As used herein, "this License" refers to version 3 of the GNU Lesser 15 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 16 | General Public License. 17 | 18 | "The Library" refers to a covered work governed by this License, 19 | other than an Application or a Combined Work as defined below. 20 | 21 | An "Application" is any work that makes use of an interface provided 22 | by the Library, but which is not otherwise based on the Library. 23 | Defining a subclass of a class defined by the Library is deemed a mode 24 | of using an interface provided by the Library. 25 | 26 | A "Combined Work" is a work produced by combining or linking an 27 | Application with the Library. The particular version of the Library 28 | with which the Combined Work was made is also called the "Linked 29 | Version". 30 | 31 | The "Minimal Corresponding Source" for a Combined Work means the 32 | Corresponding Source for the Combined Work, excluding any source code 33 | for portions of the Combined Work that, considered in isolation, are 34 | based on the Application, and not on the Linked Version. 35 | 36 | The "Corresponding Application Code" for a Combined Work means the 37 | object code and/or source code for the Application, including any data 38 | and utility programs needed for reproducing the Combined Work from the 39 | Application, but excluding the System Libraries of the Combined Work. 40 | 41 | 1. Exception to Section 3 of the GNU GPL. 42 | 43 | You may convey a covered work under sections 3 and 4 of this License 44 | without being bound by section 3 of the GNU GPL. 45 | 46 | 2. Conveying Modified Versions. 47 | 48 | If you modify a copy of the Library, and, in your modifications, a 49 | facility refers to a function or data to be supplied by an Application 50 | that uses the facility (other than as an argument passed when the 51 | facility is invoked), then you may convey a copy of the modified 52 | version: 53 | 54 | a) under this License, provided that you make a good faith effort to 55 | ensure that, in the event an Application does not supply the 56 | function or data, the facility still operates, and performs 57 | whatever part of its purpose remains meaningful, or 58 | 59 | b) under the GNU GPL, with none of the additional permissions of 60 | this License applicable to that copy. 61 | 62 | 3. Object Code Incorporating Material from Library Header Files. 63 | 64 | The object code form of an Application may incorporate material from 65 | a header file that is part of the Library. You may convey such object 66 | code under terms of your choice, provided that, if the incorporated 67 | material is not limited to numerical parameters, data structure 68 | layouts and accessors, or small macros, inline functions and templates 69 | (ten or fewer lines in length), you do both of the following: 70 | 71 | a) Give prominent notice with each copy of the object code that the 72 | Library is used in it and that the Library and its use are 73 | covered by this License. 74 | 75 | b) Accompany the object code with a copy of the GNU GPL and this license 76 | document. 77 | 78 | 4. Combined Works. 79 | 80 | You may convey a Combined Work under terms of your choice that, 81 | taken together, effectively do not restrict modification of the 82 | portions of the Library contained in the Combined Work and reverse 83 | engineering for debugging such modifications, if you also do each of 84 | the following: 85 | 86 | a) Give prominent notice with each copy of the Combined Work that 87 | the Library is used in it and that the Library and its use are 88 | covered by this License. 89 | 90 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 91 | document. 92 | 93 | c) For a Combined Work that displays copyright notices during 94 | execution, include the copyright notice for the Library among 95 | these notices, as well as a reference directing the user to the 96 | copies of the GNU GPL and this license document. 97 | 98 | d) Do one of the following: 99 | 100 | 0) Convey the Minimal Corresponding Source under the terms of this 101 | License, and the Corresponding Application Code in a form 102 | suitable for, and under terms that permit, the user to 103 | recombine or relink the Application with a modified version of 104 | the Linked Version to produce a modified Combined Work, in the 105 | manner specified by section 6 of the GNU GPL for conveying 106 | Corresponding Source. 107 | 108 | 1) Use a suitable shared library mechanism for linking with the 109 | Library. A suitable mechanism is one that (a) uses at run time 110 | a copy of the Library already present on the user's computer 111 | system, and (b) will operate properly with a modified version 112 | of the Library that is interface-compatible with the Linked 113 | Version. 114 | 115 | e) Provide Installation Information, but only if you would otherwise 116 | be required to provide such information under section 6 of the 117 | GNU GPL, and only to the extent that such information is 118 | necessary to install and execute a modified version of the 119 | Combined Work produced by recombining or relinking the 120 | Application with a modified version of the Linked Version. (If 121 | you use option 4d0, the Installation Information must accompany 122 | the Minimal Corresponding Source and Corresponding Application 123 | Code. If you use option 4d1, you must provide the Installation 124 | Information in the manner specified by section 6 of the GNU GPL 125 | for conveying Corresponding Source.) 126 | 127 | 5. Combined Libraries. 128 | 129 | You may place library facilities that are a work based on the 130 | Library side by side in a single library together with other library 131 | facilities that are not Applications and are not covered by this 132 | License, and convey such a combined library under terms of your 133 | choice, if you do both of the following: 134 | 135 | a) Accompany the combined library with a copy of the same work based 136 | on the Library, uncombined with any other library facilities, 137 | conveyed under the terms of this License. 138 | 139 | b) Give prominent notice with the combined library that part of it 140 | is a work based on the Library, and explaining where to find the 141 | accompanying uncombined form of the same work. 142 | 143 | 6. Revised Versions of the GNU Lesser General Public License. 144 | 145 | The WalletConnect Association may publish revised and/or new versions 146 | of the GNU Lesser General Public License from time to time. Such new 147 | versions will be similar in spirit to the present version, but may 148 | differ in detail to address new problems or concerns. 149 | 150 | Each version is given a distinguishing version number. If the 151 | Library as you received it specifies that a certain numbered version 152 | of the GNU Lesser General Public License "or any later version" 153 | applies to it, you have the option of following the terms and 154 | conditions either of that published version or of any later version 155 | published by the WalletConnect Association. If the Library as you 156 | received it does not specify a version number of the GNU Lesser 157 | General Public License, you may choose any version of the GNU Lesser 158 | General Public License ever published by the WalletConnect Association. 159 | 160 | If the Library as you received it specifies that a proxy can decide 161 | whether future versions of the GNU Lesser General Public License shall 162 | apply, that proxy's public statement of acceptance of any version is 163 | permanent authorization for you to choose that version for the 164 | Library. 165 | -------------------------------------------------------------------------------- /src/scenarios.ts: -------------------------------------------------------------------------------- 1 | import algosdk from "algosdk"; 2 | import { apiGetTxnParams, ChainType } from "./helpers/api"; 3 | 4 | const testAccounts = [ 5 | algosdk.mnemonicToSecretKey( 6 | "cannon scatter chest item way pulp seminar diesel width tooth enforce fire rug mushroom tube sustain glide apple radar chronic ask plastic brown ability badge", 7 | ), 8 | algosdk.mnemonicToSecretKey( 9 | "person congress dragon morning road sweet horror famous bomb engine eager silent home slam civil type melt field dry daring wheel monitor custom above term", 10 | ), 11 | algosdk.mnemonicToSecretKey( 12 | "faint protect home drink journey humble tube clinic game rough conduct sell violin discover limit lottery anger baby leaf mountain peasant rude scene abstract casual", 13 | ), 14 | ]; 15 | 16 | export function signTxnWithTestAccount(txn: algosdk.Transaction): Uint8Array { 17 | const sender = algosdk.encodeAddress(txn.from.publicKey); 18 | 19 | for (const testAccount of testAccounts) { 20 | if (testAccount.addr === sender) { 21 | return txn.signTxn(testAccount.sk); 22 | } 23 | } 24 | 25 | throw new Error(`Cannot sign transaction from unknown test account: ${sender}`); 26 | } 27 | 28 | export interface IScenarioTxn { 29 | txn: algosdk.Transaction; 30 | signers?: string[]; 31 | authAddr?: string; 32 | message?: string; 33 | } 34 | 35 | export type ScenarioReturnType = IScenarioTxn[][]; 36 | 37 | export type Scenario = (chain: ChainType, address: string) => Promise; 38 | 39 | function getAssetIndex(chain: ChainType): number { 40 | if (chain === ChainType.MainNet) { 41 | // MainNet USDC 42 | return 31566704; 43 | } 44 | 45 | if (chain === ChainType.TestNet) { 46 | // TestNet USDC 47 | return 10458941; 48 | } 49 | 50 | throw new Error(`Asset not defined for chain ${chain}`); 51 | } 52 | 53 | function getAssetReserve(chain: ChainType): string { 54 | if (chain === ChainType.MainNet) { 55 | return "2UEQTE5QDNXPI7M3TU44G6SYKLFWLPQO7EBZM7K7MHMQQMFI4QJPLHQFHM"; 56 | } 57 | 58 | if (chain === ChainType.TestNet) { 59 | return "UJBZPEMXLD6KZOLUBUDSZ3DXECXYDADZZLBH6O7CMYXHE2PLTCW44VK5T4"; 60 | } 61 | 62 | throw new Error(`Asset reserve not defined for chain ${chain}`); 63 | } 64 | 65 | function getAppIndex(chain: ChainType): number { 66 | if (chain === ChainType.MainNet) { 67 | return 305162725; 68 | } 69 | 70 | if (chain === ChainType.TestNet) { 71 | return 22314999; 72 | } 73 | 74 | throw new Error(`App not defined for chain ${chain}`); 75 | } 76 | 77 | const singlePayTxn: Scenario = async ( 78 | chain: ChainType, 79 | address: string, 80 | ): Promise => { 81 | const suggestedParams = await apiGetTxnParams(chain); 82 | 83 | const txn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ 84 | from: address, 85 | to: address, 86 | amount: 100000, 87 | note: new Uint8Array(Buffer.from("example note value")), 88 | suggestedParams, 89 | }); 90 | 91 | const txnsToSign = [ 92 | { 93 | txn, 94 | message: "This is a payment transaction that sends 0.1 Algos to yourself.", 95 | }, 96 | ]; 97 | return [txnsToSign]; 98 | }; 99 | 100 | const singleAssetOptInTxn: Scenario = async ( 101 | chain: ChainType, 102 | address: string, 103 | ): Promise => { 104 | const suggestedParams = await apiGetTxnParams(chain); 105 | const assetIndex = getAssetIndex(chain); 106 | 107 | const txn = algosdk.makeAssetTransferTxnWithSuggestedParamsFromObject({ 108 | from: address, 109 | to: address, 110 | amount: 0, 111 | assetIndex, 112 | note: new Uint8Array(Buffer.from("example note value")), 113 | suggestedParams, 114 | }); 115 | 116 | const txnsToSign = [ 117 | { 118 | txn, 119 | message: "This transaction opts you into the USDC asset if you have not already opted in.", 120 | }, 121 | ]; 122 | return [txnsToSign]; 123 | }; 124 | 125 | const singleAssetTransferTxn: Scenario = async ( 126 | chain: ChainType, 127 | address: string, 128 | ): Promise => { 129 | const suggestedParams = await apiGetTxnParams(chain); 130 | const assetIndex = getAssetIndex(chain); 131 | 132 | const txn = algosdk.makeAssetTransferTxnWithSuggestedParamsFromObject({ 133 | from: address, 134 | to: address, 135 | amount: 1000000, 136 | assetIndex, 137 | note: new Uint8Array(Buffer.from("example note value")), 138 | suggestedParams, 139 | }); 140 | 141 | const txnsToSign = [{ txn, message: "This transaction will send 1 USDC to yourself." }]; 142 | return [txnsToSign]; 143 | }; 144 | 145 | const singleAssetCloseTxn: Scenario = async ( 146 | chain: ChainType, 147 | address: string, 148 | ): Promise => { 149 | const suggestedParams = await apiGetTxnParams(chain); 150 | const assetIndex = getAssetIndex(chain); 151 | 152 | const txn = algosdk.makeAssetTransferTxnWithSuggestedParamsFromObject({ 153 | from: address, 154 | to: getAssetReserve(chain), 155 | amount: 0, 156 | assetIndex, 157 | note: new Uint8Array(Buffer.from("example note value")), 158 | closeRemainderTo: testAccounts[1].addr, 159 | suggestedParams, 160 | }); 161 | 162 | const txnsToSign = [ 163 | { 164 | txn, 165 | message: 166 | "This transaction will opt you out of the USDC asset. DO NOT submit this to MainNet if you have more than 0 USDC.", 167 | }, 168 | ]; 169 | return [txnsToSign]; 170 | }; 171 | 172 | const singleAppOptIn: Scenario = async ( 173 | chain: ChainType, 174 | address: string, 175 | ): Promise => { 176 | const suggestedParams = await apiGetTxnParams(chain); 177 | 178 | const appIndex = getAppIndex(chain); 179 | 180 | const txn = algosdk.makeApplicationOptInTxnFromObject({ 181 | from: address, 182 | appIndex, 183 | note: new Uint8Array(Buffer.from("example note value")), 184 | appArgs: [Uint8Array.from([0]), Uint8Array.from([0, 1])], 185 | suggestedParams, 186 | }); 187 | 188 | const txnsToSign = [{ txn, message: "This transaction will opt you into a test app." }]; 189 | return [txnsToSign]; 190 | }; 191 | 192 | const singleAppCall: Scenario = async ( 193 | chain: ChainType, 194 | address: string, 195 | ): Promise => { 196 | const suggestedParams = await apiGetTxnParams(chain); 197 | 198 | const appIndex = getAppIndex(chain); 199 | 200 | const txn = algosdk.makeApplicationNoOpTxnFromObject({ 201 | from: address, 202 | appIndex, 203 | note: new Uint8Array(Buffer.from("example note value")), 204 | appArgs: [Uint8Array.from([0]), Uint8Array.from([0, 1])], 205 | suggestedParams, 206 | }); 207 | 208 | const txnsToSign = [{ txn, message: "This transaction will invoke an app call on a test app." }]; 209 | return [txnsToSign]; 210 | }; 211 | 212 | const singleAppCloseOut: Scenario = async ( 213 | chain: ChainType, 214 | address: string, 215 | ): Promise => { 216 | const suggestedParams = await apiGetTxnParams(chain); 217 | 218 | const appIndex = getAppIndex(chain); 219 | 220 | const txn = algosdk.makeApplicationCloseOutTxnFromObject({ 221 | from: address, 222 | appIndex, 223 | note: new Uint8Array(Buffer.from("example note value")), 224 | appArgs: [Uint8Array.from([0]), Uint8Array.from([0, 1])], 225 | suggestedParams, 226 | }); 227 | 228 | const txnsToSign = [{ txn, message: "This transaction will opt you out of the test app." }]; 229 | return [txnsToSign]; 230 | }; 231 | 232 | const singleAppClearState: Scenario = async ( 233 | chain: ChainType, 234 | address: string, 235 | ): Promise => { 236 | const suggestedParams = await apiGetTxnParams(chain); 237 | 238 | const appIndex = getAppIndex(chain); 239 | 240 | const txn = algosdk.makeApplicationClearStateTxnFromObject({ 241 | from: address, 242 | appIndex, 243 | note: new Uint8Array(Buffer.from("example note value")), 244 | appArgs: [Uint8Array.from([0]), Uint8Array.from([0, 1])], 245 | suggestedParams, 246 | }); 247 | 248 | const txnsToSign = [ 249 | { txn, message: "This transaction will forcibly opt you out of the test app." }, 250 | ]; 251 | return [txnsToSign]; 252 | }; 253 | 254 | export const scenarios: Array<{ name: string; scenario: Scenario }> = [ 255 | { 256 | name: "1. Sign pay txn", 257 | scenario: singlePayTxn, 258 | }, 259 | { 260 | name: "2. Sign asset opt-in txn", 261 | scenario: singleAssetOptInTxn, 262 | }, 263 | { 264 | name: "3. Sign asset transfer txn", 265 | scenario: singleAssetTransferTxn, 266 | }, 267 | { 268 | name: "4. Sign asset close out txn", 269 | scenario: singleAssetCloseTxn, 270 | }, 271 | { 272 | name: "5. Sign app opt-in txn", 273 | scenario: singleAppOptIn, 274 | }, 275 | { 276 | name: "6. Sign app call txn", 277 | scenario: singleAppCall, 278 | }, 279 | { 280 | name: "7. Sign app close out txn", 281 | scenario: singleAppCloseOut, 282 | }, 283 | { 284 | name: "8. Sign app clear state txn", 285 | scenario: singleAppClearState, 286 | }, 287 | ]; 288 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import styled from "styled-components"; 3 | import WalletConnect from "@walletconnect/client"; 4 | import QRCodeModal from "algorand-walletconnect-qrcode-modal"; 5 | import { IInternalEvent } from "@walletconnect/types"; 6 | import { formatJsonRpcRequest } from "@json-rpc-tools/utils"; 7 | import algosdk from "algosdk"; 8 | import Button from "./components/Button"; 9 | import Column from "./components/Column"; 10 | import Wrapper from "./components/Wrapper"; 11 | import Modal from "./components/Modal"; 12 | import Header from "./components/Header"; 13 | import Loader from "./components/Loader"; 14 | import { fonts } from "./styles"; 15 | import { apiGetAccountAssets, apiSubmitTransactions, ChainType } from "./helpers/api"; 16 | import { IAssetData, IWalletTransaction, SignTxnParams } from "./helpers/types"; 17 | import AccountAssets from "./components/AccountAssets"; 18 | import { Scenario, scenarios, signTxnWithTestAccount } from "./scenarios"; 19 | 20 | const SLayout = styled.div` 21 | position: relative; 22 | width: 100%; 23 | /* height: 100%; */ 24 | min-height: 100vh; 25 | text-align: center; 26 | `; 27 | 28 | const SContent = styled(Wrapper as any)` 29 | width: 100%; 30 | height: 100%; 31 | padding: 0 16px; 32 | `; 33 | 34 | const SLanding = styled(Column as any)` 35 | height: 600px; 36 | `; 37 | 38 | const SButtonContainer = styled(Column as any)` 39 | width: 250px; 40 | margin: 50px 0; 41 | `; 42 | 43 | const SConnectButton = styled(Button as any)` 44 | border-radius: 8px; 45 | font-size: ${fonts.size.medium}; 46 | height: 44px; 47 | width: 100%; 48 | margin: 12px 0; 49 | `; 50 | 51 | const SContainer = styled.div` 52 | height: 100%; 53 | min-height: 200px; 54 | display: flex; 55 | flex-direction: column; 56 | justify-content: center; 57 | align-items: center; 58 | word-break: break-word; 59 | `; 60 | 61 | const SModalContainer = styled.div` 62 | width: 100%; 63 | position: relative; 64 | word-wrap: break-word; 65 | `; 66 | 67 | const SModalTitle = styled.div` 68 | margin: 1em 0; 69 | font-size: 20px; 70 | font-weight: 700; 71 | `; 72 | 73 | const SModalButton = styled.button` 74 | margin: 1em 0; 75 | font-size: 18px; 76 | font-weight: 700; 77 | `; 78 | 79 | const SModalParagraph = styled.p` 80 | margin-top: 30px; 81 | `; 82 | 83 | // @ts-ignore 84 | const SBalances = styled(SLanding as any)` 85 | height: 100%; 86 | & h3 { 87 | padding-top: 30px; 88 | } 89 | `; 90 | 91 | const STable = styled(SContainer as any)` 92 | flex-direction: column; 93 | text-align: left; 94 | `; 95 | 96 | const SRow = styled.div` 97 | width: 100%; 98 | display: flex; 99 | margin: 6px 0; 100 | `; 101 | 102 | const SKey = styled.div` 103 | width: 30%; 104 | font-weight: 700; 105 | `; 106 | 107 | const SValue = styled.div` 108 | width: 70%; 109 | font-family: monospace; 110 | `; 111 | 112 | const STestButtonContainer = styled.div` 113 | width: 100%; 114 | display: flex; 115 | justify-content: center; 116 | align-items: center; 117 | flex-wrap: wrap; 118 | `; 119 | 120 | const STestButton = styled(Button as any)` 121 | border-radius: 8px; 122 | font-size: ${fonts.size.medium}; 123 | height: 64px; 124 | width: 100%; 125 | max-width: 175px; 126 | margin: 12px; 127 | `; 128 | 129 | interface IResult { 130 | method: string; 131 | body: Array< 132 | Array<{ 133 | txID: string; 134 | signingAddress?: string; 135 | signature: string; 136 | } | null> 137 | >; 138 | } 139 | 140 | interface IAppState { 141 | connector: WalletConnect | null; 142 | fetching: boolean; 143 | connected: boolean; 144 | showModal: boolean; 145 | pendingRequest: boolean; 146 | signedTxns: Uint8Array[][] | null; 147 | pendingSubmissions: Array; 148 | uri: string; 149 | accounts: string[]; 150 | address: string; 151 | result: IResult | null; 152 | chain: ChainType; 153 | assets: IAssetData[]; 154 | } 155 | 156 | const INITIAL_STATE: IAppState = { 157 | connector: null, 158 | fetching: false, 159 | connected: false, 160 | showModal: false, 161 | pendingRequest: false, 162 | signedTxns: null, 163 | pendingSubmissions: [], 164 | uri: "", 165 | accounts: [], 166 | address: "", 167 | result: null, 168 | chain: ChainType.TestNet, 169 | assets: [], 170 | }; 171 | 172 | class App extends React.Component { 173 | public state: IAppState = { 174 | ...INITIAL_STATE, 175 | }; 176 | 177 | public walletConnectInit = async () => { 178 | // bridge url 179 | const bridge = "https://bridge.walletconnect.org"; 180 | 181 | // create new connector 182 | const connector = new WalletConnect({ bridge, qrcodeModal: QRCodeModal }); 183 | 184 | await this.setState({ connector }); 185 | 186 | // check if already connected 187 | if (!connector.connected) { 188 | // create new session 189 | await connector.createSession(); 190 | } 191 | 192 | // subscribe to events 193 | await this.subscribeToEvents(); 194 | }; 195 | public subscribeToEvents = () => { 196 | const { connector } = this.state; 197 | 198 | if (!connector) { 199 | return; 200 | } 201 | 202 | connector.on("session_update", async (error, payload) => { 203 | console.log(`connector.on("session_update")`); 204 | 205 | if (error) { 206 | throw error; 207 | } 208 | 209 | const { accounts } = payload.params[0]; 210 | this.onSessionUpdate(accounts); 211 | }); 212 | 213 | connector.on("connect", (error, payload) => { 214 | console.log(`connector.on("connect")`); 215 | 216 | if (error) { 217 | throw error; 218 | } 219 | 220 | this.onConnect(payload); 221 | }); 222 | 223 | connector.on("disconnect", (error, payload) => { 224 | console.log(`connector.on("disconnect")`); 225 | 226 | if (error) { 227 | throw error; 228 | } 229 | 230 | this.onDisconnect(); 231 | }); 232 | 233 | if (connector.connected) { 234 | const { accounts } = connector; 235 | const address = accounts[0]; 236 | this.setState({ 237 | connected: true, 238 | accounts, 239 | address, 240 | }); 241 | this.onSessionUpdate(accounts); 242 | } 243 | 244 | this.setState({ connector }); 245 | }; 246 | 247 | public killSession = async () => { 248 | const { connector } = this.state; 249 | if (connector) { 250 | connector.killSession(); 251 | } 252 | this.resetApp(); 253 | }; 254 | 255 | public chainUpdate = (newChain: ChainType) => { 256 | this.setState({ chain: newChain }, this.getAccountAssets); 257 | }; 258 | 259 | public resetApp = async () => { 260 | await this.setState({ ...INITIAL_STATE }); 261 | }; 262 | 263 | public onConnect = async (payload: IInternalEvent) => { 264 | const { accounts } = payload.params[0]; 265 | const address = accounts[0]; 266 | await this.setState({ 267 | connected: true, 268 | accounts, 269 | address, 270 | }); 271 | this.getAccountAssets(); 272 | }; 273 | 274 | public onDisconnect = async () => { 275 | this.resetApp(); 276 | }; 277 | 278 | public onSessionUpdate = async (accounts: string[]) => { 279 | const address = accounts[0]; 280 | await this.setState({ accounts, address }); 281 | await this.getAccountAssets(); 282 | }; 283 | 284 | public getAccountAssets = async () => { 285 | const { address, chain } = this.state; 286 | this.setState({ fetching: true }); 287 | try { 288 | // get account balances 289 | const assets = await apiGetAccountAssets(chain, address); 290 | 291 | await this.setState({ fetching: false, address, assets }); 292 | } catch (error) { 293 | console.error(error); 294 | await this.setState({ fetching: false }); 295 | } 296 | }; 297 | 298 | public toggleModal = () => 299 | this.setState({ 300 | showModal: !this.state.showModal, 301 | pendingSubmissions: [], 302 | }); 303 | 304 | public signTxnScenario = async (scenario: Scenario) => { 305 | const { connector, address, chain } = this.state; 306 | 307 | if (!connector) { 308 | return; 309 | } 310 | 311 | try { 312 | const txnsToSign = await scenario(chain, address); 313 | 314 | // open modal 315 | this.toggleModal(); 316 | 317 | // toggle pending request indicator 318 | this.setState({ pendingRequest: true }); 319 | 320 | const flatTxns = txnsToSign.reduce((acc, val) => acc.concat(val), []); 321 | 322 | const walletTxns: IWalletTransaction[] = flatTxns.map( 323 | ({ txn, signers, authAddr, message }) => ({ 324 | txn: Buffer.from(algosdk.encodeUnsignedTransaction(txn)).toString("base64"), 325 | signers, // TODO: put auth addr in signers array 326 | authAddr, 327 | message, 328 | }), 329 | ); 330 | 331 | // sign transaction 332 | const requestParams: SignTxnParams = [walletTxns]; 333 | const request = formatJsonRpcRequest("algo_signTxn", requestParams); 334 | const result: Array = await connector.sendCustomRequest(request); 335 | 336 | console.log("Raw response:", result); 337 | 338 | const indexToGroup = (index: number) => { 339 | for (let group = 0; group < txnsToSign.length; group++) { 340 | const groupLength = txnsToSign[group].length; 341 | if (index < groupLength) { 342 | return [group, index]; 343 | } 344 | 345 | index -= groupLength; 346 | } 347 | 348 | throw new Error(`Index too large for groups: ${index}`); 349 | }; 350 | 351 | const signedPartialTxns: Array> = txnsToSign.map(() => []); 352 | result.forEach((r, i) => { 353 | const [group, groupIndex] = indexToGroup(i); 354 | const toSign = txnsToSign[group][groupIndex]; 355 | 356 | if (r == null) { 357 | if (toSign.signers !== undefined && toSign.signers?.length < 1) { 358 | signedPartialTxns[group].push(null); 359 | return; 360 | } 361 | throw new Error(`Transaction at index ${i}: was not signed when it should have been`); 362 | } 363 | 364 | if (toSign.signers !== undefined && toSign.signers?.length < 1) { 365 | throw new Error(`Transaction at index ${i} was signed when it should not have been`); 366 | } 367 | 368 | const rawSignedTxn = Buffer.from(r, "base64"); 369 | signedPartialTxns[group].push(new Uint8Array(rawSignedTxn)); 370 | }); 371 | 372 | const signedTxns: Uint8Array[][] = signedPartialTxns.map( 373 | (signedPartialTxnsInternal, group) => { 374 | return signedPartialTxnsInternal.map((stxn, groupIndex) => { 375 | if (stxn) { 376 | return stxn; 377 | } 378 | 379 | return signTxnWithTestAccount(txnsToSign[group][groupIndex].txn); 380 | }); 381 | }, 382 | ); 383 | 384 | const signedTxnInfo: Array> = signedPartialTxns.map((signedPartialTxnsInternal, group) => { 389 | return signedPartialTxnsInternal.map((rawSignedTxn, i) => { 390 | if (rawSignedTxn == null) { 391 | return null; 392 | } 393 | 394 | const signedTxn = algosdk.decodeSignedTransaction(rawSignedTxn); 395 | const txn = (signedTxn.txn as unknown) as algosdk.Transaction; 396 | const txID = txn.txID(); 397 | const unsignedTxID = txnsToSign[group][i].txn.txID(); 398 | 399 | if (txID !== unsignedTxID) { 400 | throw new Error( 401 | `Signed transaction at index ${i} differs from unsigned transaction. Got ${txID}, expected ${unsignedTxID}`, 402 | ); 403 | } 404 | 405 | if (!signedTxn.sig) { 406 | throw new Error(`Signature not present on transaction at index ${i}`); 407 | } 408 | 409 | return { 410 | txID, 411 | signingAddress: signedTxn.sgnr ? algosdk.encodeAddress(signedTxn.sgnr) : undefined, 412 | signature: Buffer.from(signedTxn.sig).toString("base64"), 413 | }; 414 | }); 415 | }); 416 | 417 | console.log("Signed txn info:", signedTxnInfo); 418 | 419 | // format displayed result 420 | const formattedResult: IResult = { 421 | method: "algo_signTxn", 422 | body: signedTxnInfo, 423 | }; 424 | 425 | // display result 426 | this.setState({ 427 | connector, 428 | pendingRequest: false, 429 | signedTxns, 430 | result: formattedResult, 431 | }); 432 | } catch (error) { 433 | console.error(error); 434 | this.setState({ connector, pendingRequest: false, result: null }); 435 | } 436 | }; 437 | 438 | public async submitSignedTransaction() { 439 | const { signedTxns, chain } = this.state; 440 | if (signedTxns == null) { 441 | throw new Error("Transactions to submit are null"); 442 | } 443 | 444 | this.setState({ pendingSubmissions: signedTxns.map(() => 0) }); 445 | 446 | signedTxns.forEach(async (signedTxn, index) => { 447 | try { 448 | const confirmedRound = await apiSubmitTransactions(chain, signedTxn); 449 | 450 | this.setState(prevState => { 451 | return { 452 | pendingSubmissions: prevState.pendingSubmissions.map((v, i) => { 453 | if (index === i) { 454 | return confirmedRound; 455 | } 456 | return v; 457 | }), 458 | }; 459 | }); 460 | 461 | console.log(`Transaction confirmed at round ${confirmedRound}`); 462 | } catch (err) { 463 | this.setState(prevState => { 464 | return { 465 | pendingSubmissions: prevState.pendingSubmissions.map((v, i) => { 466 | if (index === i) { 467 | return err; 468 | } 469 | return v; 470 | }), 471 | }; 472 | }); 473 | 474 | console.error(`Error submitting transaction at index ${index}:`, err); 475 | } 476 | }); 477 | } 478 | 479 | public render = () => { 480 | const { 481 | chain, 482 | assets, 483 | address, 484 | connected, 485 | fetching, 486 | showModal, 487 | pendingRequest, 488 | pendingSubmissions, 489 | result, 490 | } = this.state; 491 | return ( 492 | 493 | 494 |
501 | 502 | {!address && !assets.length ? ( 503 | 504 |

{`Algorand WalletConnect v${process.env.REACT_APP_VERSION} Demo`}

505 | 506 | 507 | {"Connect to WalletConnect"} 508 | 509 | 510 |
511 | ) : ( 512 | 513 |

Balances

514 | {!fetching ? ( 515 | 516 | ) : ( 517 | 518 | 519 | 520 | 521 | 522 | )} 523 |

Actions

524 | 525 | 526 | {scenarios.map(({ name, scenario }) => ( 527 | this.signTxnScenario(scenario)}> 528 | {name} 529 | 530 | ))} 531 | 532 | 533 |
534 | )} 535 |
536 | 537 | 538 | {pendingRequest ? ( 539 | 540 | {"Pending Call Request"} 541 | 542 | 543 | {"Approve or reject request using your wallet"} 544 | 545 | 546 | ) : result ? ( 547 | 548 | {"Call Request Approved"} 549 | 550 | 551 | Method 552 | {result.method} 553 | 554 | {result.body.map((signedTxns, index) => ( 555 | 556 | {`Atomic group ${index}`} 557 | 558 | {signedTxns.map((txn, txnIndex) => ( 559 |
560 | {!!txn?.txID &&

TxID: {txn.txID}

} 561 | {!!txn?.signature &&

Sig: {txn.signature}

} 562 | {!!txn?.signingAddress &&

AuthAddr: {txn.signingAddress}

} 563 |
564 | ))} 565 |
566 |
567 | ))} 568 |
569 | this.submitSignedTransaction()} 571 | disabled={pendingSubmissions.length !== 0} 572 | > 573 | {"Submit transaction to network."} 574 | 575 | {pendingSubmissions.map((submissionInfo, index) => { 576 | const key = `${index}:${ 577 | typeof submissionInfo === "number" ? submissionInfo : "err" 578 | }`; 579 | const prefix = `Txn Group ${index}: `; 580 | let content: string; 581 | 582 | if (submissionInfo === 0) { 583 | content = "Submitting..."; 584 | } else if (typeof submissionInfo === "number") { 585 | content = `Confirmed at round ${submissionInfo}`; 586 | } else { 587 | content = "Rejected by network. See console for more information."; 588 | } 589 | 590 | return {prefix + content}; 591 | })} 592 |
593 | ) : ( 594 | 595 | {"Call Request Rejected"} 596 | 597 | )} 598 |
599 | 600 | ); 601 | }; 602 | } 603 | 604 | export default App; 605 | --------------------------------------------------------------------------------