├── 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 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------