├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierrc.js ├── LICENSE ├── README.md ├── jest.config.cjs ├── package.json ├── src ├── PlugKeyRing │ ├── constants.ts │ ├── index.ts │ ├── interfaces.ts │ ├── modules │ │ └── NetworkModule │ │ │ ├── Network.ts │ │ │ └── index.ts │ ├── plugKeyRing.test.ts │ └── utils.ts ├── PlugWallet │ └── index.ts ├── constants │ ├── tokens.ts │ └── version.ts ├── errors.ts ├── idls │ ├── canister.did.ts │ ├── icns_registry.did.ts │ ├── icns_resolver.did.ts │ ├── icns_reverse_registrar.did.ts │ ├── ledger.did.ts │ ├── nns_uid.did.ts │ └── walltet.ts ├── index.ts ├── interfaces │ ├── account.ts │ ├── cap.ts │ ├── contact_registry.ts │ ├── dank.ts │ ├── icns_registry.ts │ ├── icns_resolver.ts │ ├── icns_reverse_registrar.ts │ ├── identity.ts │ ├── ledger.ts │ ├── nns_uid.ts │ ├── plug_keyring.ts │ ├── plug_wallet.ts │ ├── storage.ts │ ├── token.ts │ ├── transactions.ts │ └── wallet.ts ├── jest │ └── setup-jest.ts └── utils │ ├── account │ ├── account.test.ts │ ├── constants.ts │ └── index.ts │ ├── array.ts │ ├── crypto │ ├── binary.ts │ └── keys.ts │ ├── dab.ts │ ├── dfx │ ├── actorFactory.ts │ ├── constants.ts │ ├── dfx.test.ts │ ├── history │ │ ├── cap │ │ │ ├── index.ts │ │ │ └── sonic.ts │ │ ├── rosetta.ts │ │ └── xtcHistory.ts │ ├── icns │ │ ├── index.ts │ │ └── utils.ts │ ├── index.ts │ ├── mockData.ts │ ├── nns_uid │ │ ├── index.ts │ │ └── methods.ts │ └── wrappedFetch.ts │ ├── formatter │ ├── constants.ts │ ├── helpers │ │ └── amountParser.ts │ ├── transaction │ │ ├── capTransactionFormatter.ts │ │ ├── ipcTransactionFormatter.ts │ │ └── xtcTransactionFormatter.ts │ └── transactionFormatter.ts │ ├── getTokensFromCollection.ts │ ├── identity │ ├── ed25519 │ │ └── ed25519Identity.ts │ ├── genericSignIdentity.ts │ ├── identityFactory.ts │ ├── parsePem.ts │ └── secpk256k1 │ │ ├── identity.ts │ │ └── publicKey.ts │ ├── idl.ts │ ├── object.ts │ ├── signature │ └── index.ts │ ├── storage │ ├── index.ts │ ├── mock.ts │ ├── update_handlers │ │ ├── v0.14.5.ts │ │ ├── v0.16.8.ts │ │ ├── v0.19.3.ts │ │ ├── v0.20.0.ts │ │ └── v0.21.0.ts │ └── utils.ts │ └── version.ts ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.spec.ts 4 | *.mock.ts 5 | *.mocks.ts -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | plugins: ['@typescript-eslint', 'eslint-plugin-import'], 4 | extends: [ 5 | 'plugin:@typescript-eslint/recommended', 6 | 'airbnb/base', 7 | 'prettier/@typescript-eslint', 8 | 'plugin:prettier/recommended', 9 | 'eslint:recommended', 10 | 'react-app', 11 | ], 12 | parserOptions: { 13 | ecmaVersion: 6, 14 | sourceType: 'module', 15 | ecmaFeatures: { 16 | jsx: true, 17 | modules: true, 18 | }, 19 | }, 20 | rules: { 21 | 'func-names': 0, 22 | '@typescript-eslint/no-namespace': 0, 23 | 'import/extensions': [ 24 | 'error', 25 | 'ignorePackages', 26 | { 27 | js: 'never', 28 | jsx: 'never', 29 | ts: 'never', 30 | tsx: 'never', 31 | }, 32 | ], 33 | 'no-useless-constructor': 'off', 34 | 'no-empty-function': 'off', 35 | }, 36 | env: { 37 | node: true, 38 | commonjs: true, 39 | jest: true, 40 | es2020: true, 41 | }, 42 | settings: { 43 | 'indent': ['error', 2], 44 | 'import/extensions': ['.js', '.jsx', '.ts', '.tsx'], 45 | 'import/parsers': { 46 | '@typescript-eslint/parser': ['.ts', '.tsx'], 47 | }, 48 | 'import/resolver': { 49 | node: { 50 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 51 | }, 52 | }, 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Oclif 107 | oclif.manifest.json 108 | 109 | .idea 110 | 111 | manual.ts -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /jest.config.js 2 | /tsconfig.json 3 | /.env 4 | /src 5 | /mock-files 6 | /coverage 7 | /dist/jest/*.js 8 | /dist/**/*.spec.js 9 | /dist/**/*.mock.js 10 | /src/manual.ts -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @psychedelic:registry=https://npm.pkg.github.com 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "es5", 3 | tabWidth: 2, 4 | semi: true, 5 | singleQuote: true, 6 | useTabs: false 7 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://storageapi.fleek.co/fleek-team-bucket/plug-banner.png) 2 | 3 | 4 | # Plug Controller - Controller functions for the Plug Extension 5 | [![Fleek](https://img.shields.io/badge/Made%20by-Fleek-blue)](https://fleek.co/) 6 | [![Discord](https://img.shields.io/badge/Discord-Channel-blue)](https://discord.gg/yVEcEzmrgm) 7 | 8 | ## Introduction 9 | 10 | The Plug Controller is a package that provides utility & logic to the Plug browser wallet extension, as well as the account creation and management. It handles the interactions between the extension and the Internet Computer as users interact with accounts, balances, canisters, and the network. 11 | 12 | ## Installation 13 | 14 | ``` 15 | npm install @psychedelic/plug-controller 16 | ``` 17 | 18 | To install the package you need to be authenticated to Github via `npm login`, ensure that you have: 19 | 20 | - A personal access token (create one [here]((https://github.com/settings/tokens))) with the `repo` and `read:packages` scopes to login to the [GitHub Package Registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#authenticating-to-github-packages). 21 | 22 | - Have authenticated via `npm login`, using the **personal access token** as your **password**: 23 | 24 | ``` 25 | npm login --registry=https://npm.pkg.github.com --scope=@Psychedelic 26 | ``` 27 | 28 | ## Plug KeyRing 29 | A Plug Keyring is a class that manages the user's accounts and allow you to create/import a mnemonic and its keypair. 30 | ``` 31 | import PlugController from '@psychedelic/plug-controller'; 32 | 33 | const keyRing = new PlugController.PlugKeyRing(); 34 | 35 | // Initialize keyring and load state from extension storage 36 | await keyRing.init(); 37 | ``` 38 | 39 | ### Keyring Creation 40 | ``` 41 | // Creates the keyring and returns the default wallet 42 | const wallet: PlugWallet = await keyRing.create(password); 43 | ``` 44 | 45 | ### Mnemonic Import 46 | ``` 47 | // Creates the keyring using the provided mnemonic and returns the default wallet 48 | const wallet: PlugWallet = await keyRing.importFromMnemonic(mnemonic, password); 49 | ``` 50 | 51 | ## Documentation 52 | 53 | Interface and Type definitions documents for the **@Psychedelic/plug-controller** implementation is provided in the following [location](https://twilight-dream-0902.on.fleek.co/). 54 | 55 | These are based in the `main release branch` and provide a good overview of the whole package (modules, IDL's, utils, etc). -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/src'], 3 | transform: { 4 | '^.+\\.tsx?$': 'ts-jest' 5 | }, 6 | transformIgnorePatterns: ['/node_modules/.*'], 7 | setupFilesAfterEnv: ['/src/jest/setup-jest.ts'], 8 | collectCoverage: true, 9 | verbose: true, 10 | coverageReporters: ['html'], 11 | coveragePathIgnorePatterns: ['/src/jest/*', '.mock.ts', '.idl', '/src/utils/storage'], 12 | preset: 'ts-jest', 13 | testEnvironment: 'node' 14 | }; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@dfinity/agent": "0.9.3", 4 | "@dfinity/candid": "0.9.3", 5 | "@dfinity/identity": "0.9.3", 6 | "@dfinity/principal": "0.9.3", 7 | "@psychedelic/cap-js": "0.0.7", 8 | "@psychedelic/dab-js": "1.6.0-alpha.2", 9 | "@types/secp256k1": "^4.0.3", 10 | "axios": "^0.21.1", 11 | "babel-jest": "^25.5.1", 12 | "bigint-conversion": "^2.2.1", 13 | "bip39": "^3.0.4", 14 | "buffer-crc32": "^0.2.13", 15 | "create-hmac": "^1.1.7", 16 | "cross-fetch": "^3.1.4", 17 | "crypto-js": "^4.0.0", 18 | "ed25519-hd-key": "^1.2.0", 19 | "extensionizer": "^1.0.1", 20 | "hdkey": "^2.0.1", 21 | "js-sha256": "^0.9.0", 22 | "json-bigint": "^1.0.0", 23 | "random-color": "^1.0.1", 24 | "reflect-metadata": "^0.1.13", 25 | "secp256k1": "^4.0.2", 26 | "text-encoding": "^0.7.0", 27 | "text-encoding-shim": "^1.0.5", 28 | "tweetnacl": "^1.0.3", 29 | "uuidv4": "^6.2.13" 30 | }, 31 | "devDependencies": { 32 | "@babel/preset-typescript": "^7.13.0", 33 | "@oclif/dev-cli": "^1.26.0", 34 | "@oclif/test": "^1.2.8", 35 | "@types/cli-progress": "^3.9.1", 36 | "@types/crypto-js": "^4.0.1", 37 | "@types/fs-extra": "^8.1.0", 38 | "@types/jest": "^25.2.3", 39 | "@types/js-yaml": "^3.12.3", 40 | "@types/node": "^13.13.52", 41 | "@types/node-fetch": "^2.5.10", 42 | "@types/parse-github-url": "^1.0.0", 43 | "@types/request-promise": "^4.1.46", 44 | "@types/text-encoding": "^0.0.35", 45 | "@typescript-eslint/eslint-plugin": "^2.27.0", 46 | "@typescript-eslint/parser": "^2.27.0", 47 | "eslint": "^6.8.0", 48 | "eslint-config-airbnb": "^18.0.1", 49 | "eslint-config-prettier": "^6.7.0", 50 | "eslint-plugin-import": "^2.19.1", 51 | "eslint-plugin-jsx-a11y": "^6.2.3", 52 | "eslint-plugin-prettier": "^3.1.2", 53 | "jest": "^25.5.4", 54 | "prettier": "^1.19.1", 55 | "test-console": "^1.1.0", 56 | "ts-jest": "^25.5.1", 57 | "ts-node": "^9.1.1", 58 | "typescript": "^4.5" 59 | }, 60 | "name": "@psychedelic/plug-controller", 61 | "version": "0.25.3", 62 | "description": "Internet Computer Plug wallet's controller", 63 | "main": "dist/index.js", 64 | "scripts": { 65 | "prebuild": "npm run export_version", 66 | "build": "npm run clean && npm run compile", 67 | "clean": "rm -rf ./dist && rm -rf tsconfig.tsbuildinfo", 68 | "compile": "tsc -b tsconfig.json", 69 | "test": "export NODE_ENV='test' && jest", 70 | "lint": "eslint --ext ts,js ./src", 71 | "watch": "tsc --watch", 72 | "publish": "npm run build && npm publish", 73 | "export_version": "node -p \"'export const PLUG_CONTROLLER_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/constants/version.ts", 74 | "add:local": "echo @psychedelic:registry=${LOCAL_REGISTRY:=http://localhost:4873/} > .npmrc && yarn add ${PKG} && echo @psychedelic:registry=https://npm.pkg.github.com > .npmrc" 75 | }, 76 | "repository": { 77 | "type": "git", 78 | "url": "git+https://github.com/Psychedelic/plug-controller.git" 79 | }, 80 | "publishConfig": { 81 | "registry": "https://npm.pkg.github.com/@psychedelic" 82 | }, 83 | "keywords": [ 84 | "ic", 85 | "dfinity", 86 | "plug", 87 | "fleek", 88 | "psychedelic", 89 | "crypto", 90 | "wallet" 91 | ], 92 | "author": "Psychedelic", 93 | "license": "MIT", 94 | "bugs": { 95 | "url": "https://github.com/Psychedelic/plug-controller/issues" 96 | }, 97 | "homepage": "https://github.com/Psychedelic/plug-controller#readme" 98 | } 99 | -------------------------------------------------------------------------------- /src/PlugKeyRing/constants.ts: -------------------------------------------------------------------------------- 1 | export const WALLET_METHODS = [ 2 | 'getNFTs', 3 | 'getBalance', 4 | 'getBalances', 5 | 'getTransactions', 6 | 'burnXTC', 7 | 'transferNFT', 8 | 'send', 9 | 'registerToken', 10 | 'registerNFT', 11 | 'removeToken', 12 | 'getTokenInfo', 13 | 'getNFTInfo', 14 | 'getICNSData', 15 | 'setReverseResolvedName', 16 | 'sign', 17 | 'getAgent', 18 | 'delegateIdentity', 19 | ]; 20 | 21 | export const MAIN_WALLET_METHODS = [ 22 | 'getContacts', 23 | 'addContact', 24 | 'deleteContact', 25 | ]; 26 | -------------------------------------------------------------------------------- /src/PlugKeyRing/index.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from 'crypto-js'; 2 | import { HttpAgent, PublicKey } from '@dfinity/agent'; 3 | import { BinaryBlob } from '@dfinity/candid'; 4 | 5 | import { 6 | NFTDetails, 7 | TokenInterfaces, 8 | } from '@psychedelic/dab-js'; 9 | import JsonBigInt from 'json-bigint'; 10 | import { v4 as uuid } from "uuid"; 11 | 12 | import PlugWallet from '../PlugWallet'; 13 | import { PlugStateStorage, PlugStateInstance } from '../interfaces/plug_keyring'; 14 | import { GetTransactionsResponse, FormattedTransactions} from '../interfaces/transactions'; 15 | import { KeyringStorage, StorageData } from '../interfaces/storage'; 16 | import { TokenBalance, StandardToken } from '../interfaces/token'; 17 | import { WalletNFTCollection, WalletNFTInfo } from '../interfaces/plug_wallet'; 18 | import { Address } from '../interfaces/contact_registry'; 19 | import { ERRORS, ERROR_CODES } from '../errors'; 20 | import { IdentityFactory } from './../utils/identity/identityFactory' 21 | import { handleStorageUpdate } from '../utils/storage/utils'; 22 | import { createAccountFromMnemonic } from '../utils/account'; 23 | import { recursiveParseBigint } from '../utils/object'; 24 | import { Types } from '../utils/account/constants'; 25 | import { createAccount } from '../utils/account'; 26 | import { getVersion } from '../utils/version'; 27 | import Storage from '../utils/storage'; 28 | 29 | import NetworkModule, { NetworkModuleParams } from './modules/NetworkModule'; 30 | import { RegisteredNFT } from './modules/NetworkModule/Network'; 31 | import { 32 | CreateAndPersistKeyRingOptions, 33 | CreateImportResponse, 34 | CreateOptions, 35 | CreatePrincipalOptions, 36 | ImportMnemonicOptions, 37 | ImportFromPemOptions, 38 | GetPrincipalFromPem, 39 | ValidatePemResponse, 40 | ImportFromSecretKey 41 | } from './interfaces'; 42 | import { WALLET_METHODS, MAIN_WALLET_METHODS } from './constants'; 43 | import { getIdentityFromPem } from './../utils/identity/parsePem' 44 | import Secp256k1KeyIdentity from '../utils/identity/secpk256k1/identity'; 45 | 46 | class PlugKeyRing { 47 | // state 48 | private state: PlugStateInstance; 49 | public isUnlocked = false; 50 | public isInitialized = false; 51 | public currentWalletId: string; 52 | public mnemonicWalletCount: number; 53 | 54 | // adapters 55 | private storage: KeyringStorage; 56 | private fetch: any; 57 | private crypto: any; // TODO: see what functions are needed and create an interface. 58 | private networkModule: NetworkModule; 59 | 60 | // wallet methods 61 | public getBalances: (args?: { subaccount?: string }) => Promise>; 62 | public getNFTs: (args?: { subaccount?: string, refresh?: boolean }) => Promise; 63 | public transferNFT: (args: { subaccount?: string; token: NFTDetails; to: string; standard: string; }) => Promise; 64 | public burnXTC: (args?: { to: string; amount: string; subaccount?: string; }) => Promise; 65 | public registerToken: (args: { canisterId: string; standard?: string; subaccount?: string; logo?: string; }) => Promise; 66 | public removeToken: (args: { canisterId: string; subaccount?: string; }) => Promise>; 67 | public getTokenInfo: (args: { canisterId: string, standard?: string, subaccount?: string }) => Promise; 68 | public getICNSData: (args: { subaccount?: string }) => Promise<{ names: string[]; reverseResolvedName: string | undefined }>; 69 | public setReverseResolvedName: (args: { name: string, subaccount?: string }) => Promise; 70 | public sign: (args: { payload: BinaryBlob, subaccount?: string }) => Promise; 71 | public getContacts: (args: { subaccount?: string }) => Promise>; 72 | public addContact: (args: { contact: Address, subaccount?: string }) => Promise; 73 | public deleteContact: (args: { addressName: string, subaccount?: string }) => Promise; 74 | public getAgent: (args?: { subaccount ?: string, host?: string }) => HttpAgent; 75 | public getBalance: (args: { token: StandardToken, subaccount?: string }) => Promise; 76 | public getTransactions: (args: { subaccount?: string, icpPrice: number }) => Promise; 77 | public send: (args: { subaccount?: string, to: string, amount: string, canisterId: string, opts?: TokenInterfaces.SendOpts }) => Promise; 78 | public delegateIdentity: (args: { to: Buffer, targets: string[], subaccount?: string }) => Promise; 79 | public getNFTInfo: (args: { canisterId: string, standard?: string, subaccount?: string }) => Promise; 80 | public registerNFT: (args: { canisterId: string, standard?: string, subaccount?: string }) => Promise; 81 | 82 | 83 | public constructor( 84 | StorageAdapter = new Storage() as KeyringStorage, 85 | CryptoAdapter = CryptoJS, 86 | FetchAdapter?: any 87 | ) { 88 | this.state = { wallets: {}, mnemonicWalletCount: 0 }; 89 | this.isUnlocked = false; 90 | this.isInitialized = false; 91 | this.currentWalletId = uuid(); 92 | this.storage = StorageAdapter; 93 | this.crypto = CryptoAdapter; 94 | this.fetch = FetchAdapter; 95 | this.networkModule = new NetworkModule({ 96 | fetch: this.fetch, 97 | storage: StorageAdapter, 98 | onNetworkChange: this.exposeWalletMethods.bind(this), 99 | }); 100 | this.exposeWalletMethods(); 101 | this.exposeMainWalletMethods(); 102 | } 103 | 104 | // Wallet proxy methods 105 | private exposeWalletMethods(): void { 106 | WALLET_METHODS.forEach(method => { 107 | this[method] = async args => { 108 | const { subaccount, ...params } = args || {}; 109 | const wallet = await this.getWallet(subaccount); 110 | await wallet.setNetwork(this.networkModule?.network); 111 | const response = await wallet[method](params); 112 | await this.updateWallet(wallet); 113 | return response; 114 | }; 115 | }); 116 | } 117 | 118 | private exposeMainWalletMethods(): void { 119 | MAIN_WALLET_METHODS.forEach(method => { 120 | this[method] = async args => { 121 | const { ...params } = args || {}; 122 | const mainAccountId = this.getMainAccountId(); 123 | const wallet = await this.getWallet(mainAccountId); 124 | await wallet.setNetwork(this.networkModule?.network); 125 | const response = await wallet[method](params); 126 | await this.updateWallet(wallet); 127 | return response; 128 | }; 129 | }); 130 | } 131 | 132 | public getPublicKey = async (subaccount?: string): Promise => { 133 | const wallet = await this.getWallet(subaccount); 134 | return wallet.publicKey; 135 | }; 136 | 137 | // Keyring aux methods 138 | private getWallet = async (subaccount?: string): Promise => { 139 | await this.checkInitialized(); 140 | this.checkUnlocked(); 141 | const uuid = (subaccount ?? this.currentWalletId); 142 | this.validateSubaccount(uuid); 143 | return this.state?.wallets[uuid]; 144 | }; 145 | 146 | private updateWallet = async (wallet: PlugWallet): Promise => { 147 | await this.checkUnlocked(); 148 | const wallets = this.state.wallets; 149 | wallets[wallet.walletId] = wallet; 150 | this.state.wallets = wallets; 151 | await this.saveEncryptedState({ wallets }, this.state.password); 152 | }; 153 | 154 | public getWalletIdFromIndex = async (index: number): Promise => { 155 | if ( 156 | index < 0 || 157 | !Number.isInteger(index) || 158 | !this.state.walletIds || 159 | index >= (this.state.walletIds.length || 0) 160 | ) { 161 | throw new Error(ERRORS.INVALID_WALLET_NUMBER); 162 | } 163 | return this.state.walletIds[index]; 164 | }; 165 | 166 | public init = async (): Promise => { 167 | const state = (await this.storage.get()) as StorageData; 168 | this.isUnlocked = !!state?.isUnlocked; 169 | this.isInitialized = !!state?.isInitialized; 170 | this.currentWalletId = state?.currentWalletId || this.currentWalletId; 171 | }; 172 | 173 | public async getMnemonic(password: string): Promise { 174 | const storage = await this.storage.get() as StorageData; 175 | const decrypted = await this.decryptState(storage?.vault, password); 176 | return decrypted.mnemonic || ''; 177 | } 178 | 179 | // Storage get 180 | private loadFromPersistance = async (password: string): Promise => { 181 | const storage = ((await this.storage.get()) || {}) as StorageData; 182 | const { vault, isInitialized, currentWalletId, version, networkModule } = storage; 183 | const networkModuleBis = networkModule; 184 | if (isInitialized && vault) { 185 | const newVersion = getVersion(); 186 | const _decrypted = 187 | newVersion !== version 188 | ? handleStorageUpdate(version, { ...this.decryptState(vault, password), networkModuleBis } ) 189 | : this.decryptState(vault, password); 190 | const { mnemonic, mnemonicWalletCount, ...decrypted } = _decrypted; 191 | this.networkModule = new NetworkModule({ 192 | ...(newVersion !== version ? (_decrypted.networkModule || {}) : networkModule), 193 | fetch: this.fetch, 194 | storage: this.storage, 195 | onNetworkChange: this.exposeWalletMethods.bind(this), 196 | }); 197 | const walletsArray = Object.values(_decrypted.wallets); 198 | const wallets = walletsArray.reduce( 199 | (walletsAccum, wallet) => ({ 200 | ...walletsAccum, 201 | [wallet.walletId]: new PlugWallet({ 202 | ...wallet, 203 | fetch: this.fetch, 204 | network: this.networkModule.network, 205 | identity: IdentityFactory.createIdentity(wallet.type, wallet.keyPair) 206 | }) 207 | }), 208 | {} 209 | ); 210 | 211 | this.state = { ...decrypted, wallets, mnemonicWalletCount }; 212 | this.isInitialized = isInitialized; 213 | this.currentWalletId = newVersion !== version ? (decrypted.currentWalletId || this.currentWalletId) : currentWalletId; 214 | this.exposeWalletMethods(); 215 | if (newVersion !== version) { 216 | await this.saveEncryptedState({ wallets, mnemonicWalletCount }, password, mnemonic); 217 | await this.storage.set({ version: newVersion, currentWalletId: this.currentWalletId }); 218 | } 219 | } 220 | }; 221 | 222 | // Key Management 223 | public create = async ({ 224 | password = '', 225 | icon, 226 | name, 227 | entropy, 228 | }: CreateOptions): Promise => { 229 | const { mnemonic } = createAccount(entropy); 230 | const wallet = await this.createAndPersistKeyRing({ 231 | mnemonic, 232 | password, 233 | icon, 234 | name, 235 | }); 236 | return { wallet, mnemonic }; 237 | }; 238 | 239 | // Key Management 240 | public importMnemonic = async ({ 241 | mnemonic, 242 | password, 243 | }: ImportMnemonicOptions): Promise => { 244 | const wallet = await this.createAndPersistKeyRing({ mnemonic, password }); 245 | return { wallet, mnemonic }; 246 | }; 247 | 248 | 249 | public importAccountFromPem = async ({ 250 | icon, 251 | name, 252 | pem, 253 | }: ImportFromPemOptions 254 | ): Promise => { 255 | await this.checkInitialized(); 256 | this.checkUnlocked(); 257 | const walletId = uuid(); 258 | const orderNumber = Object.keys(this.state.wallets).length; 259 | const { identity, type } = getIdentityFromPem(pem); 260 | const wallet = new PlugWallet({ 261 | icon, 262 | name, 263 | walletId, 264 | orderNumber, 265 | fetch: this.fetch, 266 | network: this.networkModule.network, 267 | type, 268 | identity, 269 | }); 270 | 271 | if (this.checkRepeatedAccount(wallet.principal)) { 272 | throw new Error(ERRORS.INVALID_ACCOUNT); 273 | } 274 | 275 | const wallets = { ...this.state.wallets, [walletId]: wallet }; 276 | this.state.wallets = wallets; 277 | await this.saveEncryptedState({ wallets }, this.state.password); 278 | return wallet; 279 | }; 280 | 281 | public importAccountFromPrivateKey = async ({ 282 | icon, 283 | name, 284 | secretKey, 285 | }: ImportFromSecretKey 286 | ): Promise => { 287 | await this.checkInitialized(); 288 | this.checkUnlocked(); 289 | const walletId = uuid(); 290 | const orderNumber = Object.keys(this.state.wallets).length; 291 | const buffSecretKey = Buffer.from(secretKey, 'hex'); 292 | const identity = Secp256k1KeyIdentity.fromSecretKey(buffSecretKey); 293 | const wallet = new PlugWallet({ 294 | icon, 295 | name, 296 | walletId, 297 | orderNumber, 298 | fetch: this.fetch, 299 | network: this.networkModule.network, 300 | type: Types.secretKey256k1, 301 | identity, 302 | }); 303 | 304 | if (this.checkRepeatedAccount(wallet.principal)) { 305 | throw new Error(ERRORS.INVALID_ACCOUNT); 306 | } 307 | 308 | const wallets = { ...this.state.wallets, [walletId]: wallet }; 309 | this.state.wallets = wallets; 310 | await this.saveEncryptedState({ wallets }, this.state.password); 311 | return wallet; 312 | }; 313 | 314 | 315 | 316 | public getPrincipalFromPem = async ({ 317 | pem, 318 | }: GetPrincipalFromPem 319 | ): Promise => { 320 | await this.checkInitialized(); 321 | this.checkUnlocked(); 322 | const { identity } = getIdentityFromPem(pem); 323 | const principal = identity.getPrincipal().toText(); 324 | 325 | return principal; 326 | }; 327 | 328 | public deleteImportedAccount = async (walletId: string): Promise => { 329 | await this.checkInitialized(); 330 | this.checkUnlocked(); 331 | const wallets = this.state.wallets 332 | 333 | if (wallets[walletId] && wallets[walletId].type == Types.mnemonic) { 334 | throw new Error(ERRORS.DELETE_ACCOUNT_ERROR); 335 | } 336 | 337 | const { [walletId]: deletedWallet, ...maintainedWallets } = wallets 338 | 339 | if (walletId == this.currentWalletId) { 340 | 341 | const currentWalletId = this.getMainAccountId(); 342 | this.currentWalletId = currentWalletId; 343 | 344 | await this.storage.set({ currentWalletId }); 345 | } 346 | await this.saveEncryptedState({ wallets: maintainedWallets }, this.state.password); 347 | this.state.wallets = maintainedWallets; 348 | }; 349 | 350 | public validatePem = async ({ 351 | pem, 352 | }: ImportFromPemOptions 353 | ): Promise => { 354 | try { 355 | const { identity } = getIdentityFromPem(pem); 356 | const principal = identity?.getPrincipal().toText(); 357 | 358 | if (this.checkRepeatedAccount(principal)) { 359 | return { isValid: false, errorType: ERROR_CODES.ADDED_ACCOUNT } 360 | } 361 | return { isValid: true } 362 | } catch { 363 | return { isValid: false, errorType: ERROR_CODES.INVALID_KEY }; 364 | } 365 | }; 366 | 367 | // This should only be used in import, not in derivation 368 | // to avoid throwing when deriving an account that had been previously imported 369 | private checkRepeatedAccount(principal: string): Object { 370 | const wallets = Object.values(this.state.wallets) 371 | if (wallets.find((wallet)=> wallet.principal == principal)) { 372 | return true 373 | } 374 | return false 375 | } 376 | 377 | // Key Management 378 | public createPrincipal = async ( 379 | opts?: CreatePrincipalOptions 380 | ): Promise => { 381 | await this.checkInitialized(); 382 | this.checkUnlocked(); 383 | const mnemonic = await this.getMnemonic(this.state.password as string); 384 | const walletId = uuid(); 385 | const walletNumber = this.state.mnemonicWalletCount; 386 | const orderNumber = Object.keys(this.state.wallets).length; 387 | const { identity } = createAccountFromMnemonic( 388 | mnemonic, 389 | walletNumber 390 | ); 391 | const wallet = new PlugWallet({ 392 | ...opts, 393 | walletId, 394 | orderNumber, 395 | fetch: this.fetch, 396 | network: this.networkModule.network, 397 | type: Types.mnemonic, 398 | identity, 399 | }); 400 | const wallets = { ...this.state.wallets, [walletId]: wallet }; 401 | await this.saveEncryptedState({ wallets, mnemonicWalletCount: walletNumber + 1 }, this.state.password); 402 | this.state.wallets = wallets; 403 | this.state.mnemonicWalletCount = walletNumber + 1; 404 | return wallet; 405 | }; 406 | 407 | // Key Management 408 | public setCurrentPrincipal = async (walletId: string): Promise => { 409 | await this.checkInitialized(); 410 | this.validateSubaccount(walletId); 411 | this.currentWalletId = walletId; 412 | await this.storage.set({ currentWalletId: walletId }); 413 | }; 414 | 415 | // General 416 | public getState = async (): Promise => { 417 | await this.checkInitialized(); 418 | this.checkUnlocked(); 419 | return recursiveParseBigint({ 420 | ...this.state, 421 | currentWalletId: this.currentWalletId, 422 | }); 423 | }; 424 | 425 | // General 426 | public unlock = async (password: string): Promise => { 427 | await this.checkInitialized(); 428 | try { 429 | await this.loadFromPersistance(password); 430 | this.isUnlocked = password === this.state?.password; 431 | await this.storage.set({ isUnlocked: this.isUnlocked }); 432 | return this.isUnlocked; 433 | } catch (e) { 434 | console.error('UNLOCK ERROR:', e); 435 | this.isUnlocked = false; 436 | return false; 437 | } 438 | }; 439 | 440 | // General 441 | public lock = async (): Promise => { 442 | this.isUnlocked = false; 443 | this.state = { wallets: {}, mnemonicWalletCount: 0 }; 444 | await this.storage.set({ isUnlocked: this.isUnlocked }); 445 | }; 446 | 447 | // Key Management 448 | public editPrincipal = async ( 449 | walletId: string, 450 | { name, emoji }: { name?: string; emoji?: string } 451 | ): Promise => { 452 | const wallet = await this.getWallet(walletId); 453 | if (name) wallet.setName(name); 454 | if (emoji) wallet.setIcon(emoji); 455 | await this.updateWallet(wallet); 456 | }; 457 | 458 | private validateSubaccount(subaccount: string): void { 459 | 460 | if ( 461 | !this.state.wallets[subaccount] 462 | ) { 463 | throw new Error(ERRORS.INVALID_WALLET_NUMBER); 464 | } 465 | } 466 | 467 | private checkInitialized = async (): Promise => { 468 | await this.init(); 469 | if (!this.isInitialized) throw new Error(ERRORS.NOT_INITIALIZED); 470 | }; 471 | 472 | public getPemFile = async (walletId?: string): Promise => { 473 | const wallet = await this.getWallet(walletId); 474 | return wallet.pemFile; 475 | }; 476 | 477 | private checkUnlocked = (): void => { 478 | if (!this.isUnlocked) { 479 | throw new Error(ERRORS.STATE_LOCKED); 480 | } 481 | }; 482 | 483 | // General 484 | private createAndPersistKeyRing = async ({ 485 | mnemonic, 486 | password, 487 | icon, 488 | name, 489 | }: CreateAndPersistKeyRingOptions): Promise => { 490 | if (!password) throw new Error(ERRORS.PASSWORD_REQUIRED); 491 | const walletId = this.currentWalletId; 492 | const { identity } = createAccountFromMnemonic( 493 | mnemonic, 494 | 0 495 | ); 496 | 497 | const wallet = new PlugWallet({ 498 | icon, 499 | name, 500 | walletId, 501 | orderNumber: 0, 502 | fetch: this.fetch, 503 | network: this.networkModule.network, 504 | identity: identity, 505 | type: Types.mnemonic, 506 | }); 507 | 508 | const data = { 509 | wallets: {[walletId]: wallet.toJSON()}, 510 | password, 511 | mnemonic, 512 | mnemonicWalletCount: 1, 513 | }; 514 | 515 | this.isInitialized = true; 516 | this.currentWalletId = walletId; 517 | this.state.mnemonicWalletCount = 1; 518 | await this.storage.clear(); 519 | await this.storage.set({ 520 | isInitialized: true, 521 | isUnlocked: true, 522 | currentWalletId: walletId, 523 | version: getVersion(), 524 | vault: this.crypto.AES.encrypt( 525 | JSON.stringify({ mnemonic }), 526 | password 527 | ).toString(), // Pre-save mnemonic in storage 528 | }); 529 | await this.saveEncryptedState(data, password); 530 | await this.unlock(password); 531 | return wallet; 532 | }; 533 | 534 | // Storage 535 | private saveEncryptedState = async ( 536 | newState, 537 | password, 538 | defaultMnemonic? 539 | ): Promise => { 540 | const mnemonic = defaultMnemonic || (await this.getMnemonic(password)); 541 | const stringData = JsonBigInt.stringify({ 542 | ...this.state, 543 | ...newState, 544 | mnemonic, 545 | }); 546 | const encrypted = this.crypto.AES.encrypt(stringData, password); 547 | await this.storage.set({ vault: encrypted.toString() }); 548 | }; 549 | 550 | // Storage 551 | private decryptState = (state, password): PlugStateStorage & { mnemonic: string, networkModule?: NetworkModuleParams } => 552 | JSON.parse( 553 | this.crypto.AES.decrypt(state, password).toString(this.crypto.enc.Utf8) 554 | ); 555 | 556 | public checkPassword = async (password: string): Promise => { 557 | await this.checkInitialized(); 558 | try { 559 | const { vault, isInitialized } = ((await this.storage.get()) || 560 | {}) as StorageData; 561 | if (isInitialized && vault) { 562 | const decrypted = this.decryptState(vault, password); 563 | return decrypted.password === password; 564 | } 565 | return false; 566 | } catch (e) { 567 | return false; 568 | } 569 | } 570 | 571 | // Utils 572 | private getMainAccountId = (): string => { 573 | const { wallets } = this.state; 574 | const mainAccount = Object.values(wallets).find( 575 | (wallet) => wallet.orderNumber === 0); 576 | 577 | return mainAccount?.walletId || this.currentWalletId; 578 | } 579 | } 580 | 581 | export default PlugKeyRing; 582 | -------------------------------------------------------------------------------- /src/PlugKeyRing/interfaces.ts: -------------------------------------------------------------------------------- 1 | import PlugWallet from "../PlugWallet"; 2 | import { ERROR_CODES } from "../errors" 3 | 4 | export interface CreateImportResponse { wallet: PlugWallet; mnemonic: string; } 5 | 6 | export interface CreatePrincipalOptions { 7 | name?: string; 8 | icon?: string; 9 | } 10 | 11 | export interface CreateOptions extends CreatePrincipalOptions { 12 | password: string; 13 | entropy?: Buffer; 14 | } 15 | 16 | export interface CreateAndPersistKeyRingOptions extends CreateOptions { 17 | mnemonic: string; 18 | } 19 | 20 | export interface ImportMnemonicOptions { 21 | mnemonic: string; 22 | password: string; 23 | } 24 | 25 | export interface ImportFromPemOptions extends CreatePrincipalOptions { 26 | pem: string; 27 | } 28 | 29 | export interface ImportFromSecretKey extends CreatePrincipalOptions { 30 | secretKey: string; 31 | } 32 | 33 | export interface GetPrincipalFromPem { 34 | pem: string; 35 | } 36 | 37 | export interface ValidatePemResponse { 38 | isValid: boolean; 39 | errorType?: string; 40 | } 41 | -------------------------------------------------------------------------------- /src/PlugKeyRing/modules/NetworkModule/Network.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from "uuid"; 2 | import { getNFTActor, getTokenActor, NFTCollection, standards } from "@psychedelic/dab-js"; 3 | import { SignIdentity } from '@dfinity/agent'; 4 | 5 | import { ERRORS } from "../../../errors"; 6 | import { validateCanisterId } from "../../utils"; 7 | import { IC_URL_HOST } from "../../../utils/dfx/constants"; 8 | import { DEFAULT_MAINNET_TOKENS, TOKENS } from "../../../constants/tokens"; 9 | import { StandardToken } from "../../../interfaces/token"; 10 | import { recursiveParseBigint } from "../../../utils/object"; 11 | import { createAgent } from "../../../utils/dfx"; 12 | 13 | export type NetworkParams = { 14 | name: string; 15 | host: string; 16 | ledgerCanisterId?: string; 17 | registeredTokens?: RegisteredToken[]; 18 | registeredNFTS?: RegisteredNFT[]; 19 | id?: string; 20 | onChange?: () => void; 21 | } 22 | 23 | export type EditNetworkParams = { 24 | name?: string; 25 | host?: string; 26 | ledgerCanisterId?: string; 27 | } 28 | 29 | export type RegisteredToken = StandardToken & { registeredBy: Array }; 30 | 31 | export type RegisteredNFT = NFTCollection & { registeredBy: Array }; 32 | // Function that takes in an array of tokens and returns an array without duplicates 33 | 34 | type uniqueTokensType = Array; 35 | 36 | export const uniqueTokens = (tokens:uniqueTokensType) => { 37 | const uniqueTokens = tokens.filter((token, index) => { 38 | return tokens.findIndex(t => t.canisterId === token.canisterId) === index; 39 | }); 40 | return uniqueTokens; 41 | } 42 | 43 | export class Network { 44 | public name: string; 45 | public host: string; 46 | public ledgerCanisterId?: string; 47 | public id: string; 48 | public isCustom: boolean; 49 | public defaultTokens: StandardToken[]; 50 | public registeredTokens: RegisteredToken[]; 51 | public registeredNFTS: RegisteredNFT[]; 52 | private onChange; 53 | private fetch: any 54 | 55 | constructor(networkParams: NetworkParams, fetch: any) { 56 | this.name = networkParams.name; 57 | this.host = networkParams.host; 58 | this.onChange = networkParams.onChange; 59 | this.id = networkParams?.id || uuid(); 60 | this.isCustom = true; 61 | this.ledgerCanisterId = networkParams.ledgerCanisterId || ''; 62 | this.defaultTokens = [{ 63 | name: 'ICP', 64 | symbol: 'ICP', 65 | canisterId: networkParams.ledgerCanisterId || '', 66 | standard: standards.TOKEN.icp, 67 | decimals: 8, 68 | }]; 69 | this.registeredTokens = [...(networkParams.registeredTokens || [])]; 70 | this.registeredNFTS = [...(networkParams.registeredNFTS || [])]; 71 | this.fetch = fetch; 72 | } 73 | 74 | 75 | get tokens(): StandardToken[] { 76 | return [...this.defaultTokens, ...this.registeredTokens]; 77 | } 78 | 79 | public tokenByCanisterId(canisterId: string): StandardToken | undefined { 80 | return this.tokens.find(token => token.canisterId === canisterId); 81 | } 82 | 83 | public edit({ name, host, ledgerCanisterId }: EditNetworkParams) { 84 | this.name = name || this.name; 85 | this.host = host || this.host; 86 | this.ledgerCanisterId = ledgerCanisterId || this.ledgerCanisterId; 87 | this.onChange?.(); 88 | } 89 | 90 | public createAgent({ defaultIdentity } : {defaultIdentity: SignIdentity}) { 91 | const agent = createAgent({ 92 | defaultIdentity, 93 | host: this.host, 94 | fetch: this.fetch, 95 | wrapped: !this.isCustom 96 | }); 97 | return agent; 98 | } 99 | 100 | public getTokenInfo = async ({ canisterId, standard, defaultIdentity }) => { 101 | if (!validateCanisterId(canisterId)) { 102 | throw new Error(ERRORS.INVALID_CANISTER_ID); 103 | } 104 | const agent = this.createAgent({ defaultIdentity }); 105 | const tokenActor = await getTokenActor({ canisterId, standard, agent }); 106 | const metadata = await tokenActor.getMetadata(); 107 | if (!('fungible' in metadata)) { 108 | throw new Error(ERRORS.NON_FUNGIBLE_TOKEN_NOT_SUPPORTED); 109 | } 110 | const token:RegisteredToken = { ...metadata.fungible, canisterId, standard, registeredBy: []}; 111 | 112 | this.registeredTokens = uniqueTokens([...this.registeredTokens, token]) as RegisteredToken[]; 113 | return token; 114 | } 115 | 116 | public getNftInfo = async ({ canisterId, identity, standard }) => { 117 | if (!validateCanisterId(canisterId)) { 118 | throw new Error(ERRORS.INVALID_CANISTER_ID); 119 | } 120 | try { 121 | const agent = this.createAgent({ defaultIdentity: identity }); 122 | const nftActor = getNFTActor({ canisterId, agent, standard }); 123 | const metadata = await nftActor.getMetadata(); 124 | const nft = {...metadata, registeredBy: []}; 125 | return nft 126 | } catch(e) { 127 | throw new Error(ERRORS.CANISTER_INTERFACE_ERROR); 128 | } 129 | } 130 | 131 | public registerNFT = async ({ 132 | canisterId, standard, walletId, identity, 133 | }) => { 134 | const nft = this.registeredNFTS.find(({ canisterId: id }) => id === canisterId); 135 | 136 | if (nft) { 137 | throw new Error(ERRORS.NFT_ALREADY_REGISTERED); 138 | } 139 | 140 | const nftInfo = await this.getNftInfo({ canisterId, identity, standard }); 141 | this.registeredNFTS = uniqueTokens([...this.registeredNFTS, nftInfo]) as RegisteredNFT[]; 142 | 143 | this.registeredNFTS = this.registeredNFTS.map(n => n.canisterId === canisterId ? {...n, registeredBy: [...n?.registeredBy, walletId]} : n); 144 | await this.onChange?.(); 145 | return this.registeredNFTS; 146 | }; 147 | 148 | public registerToken = async ({ canisterId, standard, walletId, defaultIdentity, logo }: { canisterId: string, standard: string, walletId: string, defaultIdentity: SignIdentity, logo?: string }) => { 149 | const token = this.registeredTokens.find(({ canisterId: id }) => id === canisterId); 150 | const defaultToken = this.defaultTokens.find(({ canisterId: id }) => id === canisterId); 151 | if (defaultToken) { 152 | return this.defaultTokens; 153 | } 154 | if (!token) { 155 | await this.getTokenInfo({ canisterId, standard, defaultIdentity }); 156 | } 157 | this.registeredTokens = this.registeredTokens.map( 158 | t => t.canisterId === canisterId ? { ...t, logo, registeredBy: [...t?.registeredBy, walletId] } : t 159 | ); 160 | await this.onChange?.(); 161 | return this.registeredTokens; 162 | } 163 | 164 | public removeToken = async ({ canisterId }: { canisterId: string }): Promise => { 165 | if (!this.registeredTokens.map(t => t.canisterId).includes(canisterId)) { 166 | return this.registeredTokens; 167 | } 168 | const newTokens = this.registeredTokens.filter(t => t.canisterId !== canisterId); 169 | this.registeredTokens = newTokens; 170 | await this.onChange?.(); 171 | return newTokens; 172 | }; 173 | 174 | public getTokens = (walletId) => { 175 | return [ 176 | ...this.defaultTokens, 177 | ...this.registeredTokens.filter(t => t?.registeredBy?.includes(walletId)), 178 | ]; 179 | } 180 | 181 | public toJSON(): Omit { 182 | return { 183 | name: this.name, 184 | host: this.host, 185 | ledgerCanisterId: this.ledgerCanisterId, 186 | registeredTokens: this.registeredTokens?.map(recursiveParseBigint), 187 | id: this.id, 188 | }; 189 | } 190 | } 191 | 192 | 193 | 194 | export class Mainnet extends Network { 195 | constructor({ registeredTokens, onChange }: { registeredTokens?: RegisteredToken[], onChange?: () => void }, fetch: any) { 196 | super({ 197 | onChange, 198 | registeredTokens, 199 | name: 'Mainnet', 200 | host: `https://${IC_URL_HOST}`, 201 | ledgerCanisterId: TOKENS.ICP.canisterId, 202 | }, fetch); 203 | this.id = 'mainnet'; 204 | this.isCustom = false; 205 | this.defaultTokens = DEFAULT_MAINNET_TOKENS; 206 | this.registeredTokens = registeredTokens || []; 207 | } 208 | 209 | public edit(): void { 210 | return; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/PlugKeyRing/modules/NetworkModule/index.ts: -------------------------------------------------------------------------------- 1 | import { ERRORS } from "../../../errors"; 2 | import { KeyringStorage } from "../../../interfaces/storage"; 3 | import { EditNetworkParams, Mainnet, Network, NetworkParams } from "./Network"; 4 | 5 | export interface NetworkModuleParams { 6 | fetch: any; 7 | networks?: { [networkId: string]: Network }; 8 | networkId?: string; 9 | storage: KeyringStorage; 10 | onNetworkChange: (network: Network) => void; 11 | }; 12 | 13 | const createNetwork = (fetch, network) => network.id === 'mainnet' ? new Mainnet(network, fetch) : new Network(network, fetch); 14 | const createNetworks = (fetch, networks?: { [id: string]: Network }, onChange?: () => void) => { 15 | if (!!Object.values(networks || {})?.length) { 16 | return Object.values(networks!)?.reduce( 17 | (acum, net) => ({ ...acum, [net.id]: createNetwork(fetch, { ...net, onChange }) }) 18 | , {}); 19 | } 20 | return { mainnet: new Mainnet({ onChange }, fetch) }; 21 | }; 22 | 23 | class NetworkModule { 24 | public networkId: string; 25 | public networks: { [networkId: string]: Network }; 26 | private fetch: any; 27 | private storage: KeyringStorage; 28 | private onNetworkChange?: (network: Network) => void; 29 | 30 | 31 | constructor({ networks, networkId, storage, onNetworkChange, fetch }: NetworkModuleParams) { 32 | this.fetch = fetch; 33 | this.storage = storage; 34 | this.onNetworkChange = onNetworkChange; 35 | this.networkId = networkId || 'mainnet'; 36 | this.networks = createNetworks(this.fetch, networks, this.update.bind(this)); 37 | } 38 | 39 | public get network(): Network { 40 | return this.networks[this.networkId]; 41 | } 42 | 43 | public updateStorage() { 44 | this.storage.set({ networkModule: this.toJSON() }); 45 | } 46 | 47 | private update() { 48 | this.updateStorage?.(); 49 | this.onNetworkChange?.(this.network); 50 | } 51 | 52 | 53 | public addNetwork(networkParams: NetworkParams) { 54 | // Validate network host is a valid https url 55 | // if (!networkParams.host.startsWith('https://')) { 56 | // throw new Error('Network must start with https://'); 57 | // } 58 | if (Object.values(this.networks).some((net) => net.host === networkParams.host)) { 59 | throw new Error(`A Network with host ${networkParams.host} already exists`); 60 | } 61 | const network = createNetwork(this.fetch, { ...networkParams, onChange: this.update.bind(this) }); 62 | this.networks = { ...this.networks, [network.id!]: network }; 63 | this.update(); 64 | return this.networks; 65 | } 66 | 67 | public removeNetwork(networkId: string) { 68 | if (networkId === 'mainnet') throw new Error('Cannot remove mainnet'); 69 | if (!Object.keys(this.networks).includes(networkId)) throw new Error(ERRORS.INVALID_NETWORK_ID); 70 | // If we remove the current network, default to mainnet. 71 | if (networkId === this.network.id) { 72 | this.networkId = 'mainnet'; 73 | } 74 | const { [networkId]: network, ...networks } = this.networks; 75 | this.networks = networks; 76 | this.update(); 77 | return this.networks; 78 | } 79 | 80 | public setNetwork(networkId: string) { 81 | const network = this.networks[networkId]; 82 | if (!network) throw new Error(ERRORS.INVALID_NETWORK_ID); 83 | this.networkId = networkId; 84 | this.update(); 85 | return network; 86 | } 87 | 88 | public editNetwork(networkId: string, params: EditNetworkParams) { 89 | const network = this.networks[networkId]; 90 | if (!network) throw new Error(ERRORS.INVALID_NETWORK_ID); 91 | network?.edit?.(params); 92 | this.networks = { ...this.networks, [networkId]: network }; 93 | this.update(); 94 | return network; 95 | } 96 | 97 | public toJSON() { 98 | return { 99 | networkId: this.networkId, 100 | networks: Object.values(this.networks).reduce( 101 | (acum, net) => ({ ...acum, [net.id]: net.toJSON() }), {}, 102 | ) 103 | }; 104 | } 105 | } 106 | 107 | export { Network }; 108 | 109 | export default NetworkModule; 110 | -------------------------------------------------------------------------------- /src/PlugKeyRing/utils.ts: -------------------------------------------------------------------------------- 1 | import { Principal } from '@dfinity/principal'; 2 | 3 | import { 4 | PRINCIPAL_REGEX, 5 | CANISTER_MAX_LENGTH, 6 | ALPHANUM_REGEX, 7 | } from '../utils/dfx/constants'; 8 | 9 | export const isValidPrincipal = (text: string): boolean => 10 | Principal.fromText(text).toText() === text; 11 | 12 | export const validatePrincipalId = (text: string): boolean => { 13 | try { 14 | return Boolean(PRINCIPAL_REGEX.test(text) && isValidPrincipal(text)); 15 | } catch (e) { 16 | return false; 17 | } 18 | }; 19 | export const validateAccountId = (text): boolean => 20 | text.length === 64 && ALPHANUM_REGEX.test(text); 21 | export const validateCanisterId = (text: string): boolean => { 22 | try { 23 | return Boolean( 24 | text.length <= CANISTER_MAX_LENGTH && isValidPrincipal(text) 25 | ); 26 | } catch (e) { 27 | return false; 28 | } 29 | }; 30 | 31 | export const validateToken = (metadata: any): boolean => 32 | Boolean(!!metadata.decimal && !!metadata.name && !!metadata.symbol); 33 | -------------------------------------------------------------------------------- /src/PlugWallet/index.ts: -------------------------------------------------------------------------------- 1 | import { HttpAgent, PublicKey, SignIdentity } from '@dfinity/agent'; 2 | import { BinaryBlob, blobFromBuffer } from '@dfinity/candid'; 3 | import { Principal } from '@dfinity/principal'; 4 | import { DelegationChain, Ed25519PublicKey } from '@dfinity/identity'; 5 | import { 6 | getCachedUserNFTs, 7 | getNFTActor, 8 | NFTCollection, 9 | NFTDetails, 10 | getTokenActor, 11 | TokenInterfaces, 12 | getAddresses, 13 | addAddress, 14 | removeAddress, 15 | getAllUserNFTs, 16 | FungibleMetadata, 17 | } from '@psychedelic/dab-js'; 18 | import randomColor from 'random-color'; 19 | 20 | 21 | import { ERRORS } from '../errors'; 22 | import { validateCanisterId, validatePrincipalId } from '../PlugKeyRing/utils'; 23 | import { createAgent } from '../utils/dfx'; 24 | import { getICPTransactions } from '../utils/dfx/history/rosetta'; 25 | import { TOKENS, DEFAULT_MAINNET_ASSETS } from '../constants/tokens'; 26 | import { 27 | getXTCTransactions, 28 | requestCacheUpdate, 29 | } from '../utils/dfx/history/xtcHistory'; 30 | import { getCapTransactions } from '../utils/dfx/history/cap'; 31 | 32 | import { ConnectedApp } from '../interfaces/account'; 33 | import { 34 | JSONWallet, 35 | ICNSData, 36 | PlugWalletArgs, 37 | WalletNFTCollection, 38 | WalletNFTInfo, 39 | } from '../interfaces/plug_wallet'; 40 | import { StandardToken, TokenBalance } from '../interfaces/token'; 41 | import { FormattedTransactions } from '../interfaces/transactions'; 42 | import ICNSAdapter from '../utils/dfx/icns'; 43 | import { 44 | recursiveFindPrincipals, 45 | replacePrincipalsForICNS, 46 | } from '../utils/dfx/icns/utils'; 47 | import { Address } from '../interfaces/contact_registry'; 48 | import { Network } from '../PlugKeyRing/modules/NetworkModule'; 49 | import { RegisteredNFT, RegisteredToken, uniqueTokens } from '../PlugKeyRing/modules/NetworkModule/Network'; 50 | import { getAccountId } from '../utils/account'; 51 | import { Types } from '../utils/account/constants'; 52 | import { GenericSignIdentity } from '../utils/identity/genericSignIdentity'; 53 | import { getTokensFromCollections } from '../utils/getTokensFromCollection'; 54 | import { Buffer } from '../../node_modules/buffer'; 55 | import { formatTransactions } from '../utils/formatter/transactionFormatter' 56 | 57 | class PlugWallet { 58 | name: string; 59 | icon?: string; 60 | walletId: string; 61 | orderNumber: number; 62 | walletNumber?: number; 63 | principal: string; 64 | fetch: any; 65 | icnsData: ICNSData; 66 | contacts: Array
; 67 | type: Types; 68 | private identity: GenericSignIdentity; 69 | private agent: HttpAgent; 70 | private network: Network; 71 | 72 | constructor({ 73 | name, 74 | icon, 75 | walletId, 76 | orderNumber, 77 | walletNumber, 78 | fetch, 79 | icnsData = {}, 80 | network, 81 | identity, 82 | type, 83 | }: PlugWalletArgs) { 84 | this.name = name || 'Account 1'; 85 | this.icon = icon; 86 | this.walletId = walletId; 87 | this.orderNumber = orderNumber; 88 | this.walletNumber = walletNumber; 89 | this.icnsData = icnsData; 90 | this.identity = identity; 91 | this.principal = identity.getPrincipal().toText(); 92 | this.fetch = fetch; 93 | this.network = network; 94 | this.type = type; 95 | this.agent = createAgent({ 96 | defaultIdentity: this.identity, 97 | fetch: this.fetch, 98 | }); 99 | } 100 | 101 | get accountId(): string { 102 | return getAccountId(this.identity.getPrincipal()); 103 | } 104 | 105 | public async setNetwork(network: Network) { 106 | this.network = network; 107 | this.agent = network.createAgent({ defaultIdentity: this.identity }); 108 | } 109 | 110 | public setName(val: string): void { 111 | this.name = val; 112 | } 113 | 114 | public async sign({ payload } : { payload: BinaryBlob }): Promise { 115 | return this.identity.sign(payload); 116 | } 117 | 118 | public setIcon(val: string): void { 119 | this.icon = val; 120 | } 121 | 122 | private populateAndTrimNFTs = async (collections: NFTCollection[]): Promise => { 123 | const icnsAdapter = new ICNSAdapter(this.agent); 124 | const collectionWithTokens = await getTokensFromCollections(this.network.registeredNFTS, this.principal, this.agent); 125 | const icnsCollection = await icnsAdapter.getICNSCollection(); 126 | const unique = uniqueTokens([...collections, icnsCollection, ...collectionWithTokens]) as WalletNFTCollection[] 127 | const simplifiedCollections = unique.map((collection: NFTCollection): WalletNFTCollection => ({ 128 | ...collection, 129 | tokens: collection.tokens.map((token) => ({ 130 | index: token.index, 131 | url: token.url, 132 | canister: token.canister, 133 | standard: token.standard, 134 | name: token.name, 135 | })), 136 | })); 137 | const completeCollections = simplifiedCollections.filter((collection) => collection.tokens.length > 0); 138 | return completeCollections; 139 | } 140 | 141 | private nativeGetNFTs = async () => { 142 | try { 143 | const collections = await getAllUserNFTs({ 144 | user: this.principal, 145 | agent: this.agent, 146 | }); 147 | const populatedCollections = await this.populateAndTrimNFTs(collections) 148 | return populatedCollections; 149 | } catch (e) { 150 | console.warn('Error when trying to fetch NFTs natively from the IC', e); 151 | return null; 152 | } 153 | }; 154 | 155 | 156 | // TODO: Make generic when standard is adopted. Just supports ICPunks rn. 157 | public getNFTs = async (args?: { 158 | refresh?: boolean; 159 | }): Promise => { 160 | if (this.network.isCustom) return []; 161 | try { 162 | const collections = await getCachedUserNFTs({ 163 | userPID: this.principal, 164 | refresh: args?.refresh, 165 | }); 166 | const populatedCollections = await this.populateAndTrimNFTs(collections); 167 | return populatedCollections; 168 | } catch (e) { 169 | console.warn( 170 | 'Error when trying to fetch NFTs from Kyasshu. Fetching natively...', 171 | e 172 | ); 173 | // If kya fails, try native integration 174 | return await this.nativeGetNFTs(); 175 | } 176 | }; 177 | 178 | public transferNFT = async (args: { 179 | token: NFTDetails; 180 | to: string; 181 | }): Promise => { 182 | const { token, to } = args; 183 | if (!validatePrincipalId(to)) { 184 | throw new Error(ERRORS.INVALID_PRINCIPAL_ID); 185 | } 186 | try { 187 | const NFT = getNFTActor({ 188 | canisterId: token.canister, 189 | agent: this.agent, 190 | standard: token.standard.toUpperCase(), 191 | }); 192 | 193 | await NFT.transfer( 194 | Principal.fromText(to), 195 | parseInt(token.index.toString(), 10) 196 | ); 197 | getCachedUserNFTs({ userPID: this.principal, refresh: true }).catch( 198 | console.warn 199 | ); 200 | return true; 201 | } catch (e) { 202 | console.warn('NFT transfer error: ', e); 203 | throw new Error(ERRORS.TRANSFER_NFT_ERROR); 204 | } 205 | }; 206 | 207 | public getTokenInfo = async ({ canisterId, standard }) => { 208 | const token = await this.network.getTokenInfo({ 209 | canisterId, 210 | standard, 211 | defaultIdentity: this.identity, 212 | }); 213 | const balance = await this.getTokenBalance({ token }); 214 | return balance; 215 | }; 216 | 217 | public getNFTInfo = async ({ canisterId, standard }): Promise => { 218 | const nft = await this.network.getNftInfo({ canisterId, identity: this.identity, standard }); 219 | return nft; 220 | } 221 | 222 | public registerNFT = async ({canisterId, standard}): Promise => { 223 | const nfts = await this.network.registerNFT({canisterId, standard, walletId: this.walletId, identity: this.identity}); 224 | const registeredNFT = nfts.find( 225 | nft => nft.canisterId === canisterId 226 | ) 227 | return registeredNFT; 228 | } 229 | 230 | public registerToken = async (args: { 231 | canisterId: string; 232 | standard: string; 233 | logo?: string; 234 | }): Promise => { 235 | const { canisterId, standard = 'ext', logo } = args || {}; 236 | 237 | // Register token in network 238 | const tokens = await this.network.registerToken({ 239 | canisterId, 240 | standard, 241 | walletId: this.walletId, 242 | defaultIdentity: this.identity, 243 | logo, 244 | }); 245 | 246 | // Get token balance 247 | const tokenActor = await getTokenActor({ 248 | canisterId, 249 | agent: this.agent, 250 | standard, 251 | }); 252 | const balance = await tokenActor.getBalance( 253 | Principal.fromText(this.principal) 254 | ); 255 | 256 | // Format token and add asset to wallet state 257 | const color = randomColor({ luminosity: 'light' }); 258 | const registeredToken = tokens.find( 259 | t => t.canisterId === canisterId 260 | ) as RegisteredToken; 261 | const tokenDescriptor = { 262 | amount: balance.value, 263 | token: { 264 | ...registeredToken, 265 | decimals: parseInt(registeredToken.decimals.toString(), 10), 266 | canisterId, 267 | color, 268 | standard, 269 | logo, 270 | }, 271 | }; 272 | return tokenDescriptor; 273 | }; 274 | 275 | public toJSON = (): JSONWallet => ({ 276 | name: this.name, 277 | walletId: this.walletId, 278 | orderNumber: this.orderNumber, 279 | walletNumber: this.walletNumber, 280 | principal: this.identity.getPrincipal().toText(), 281 | accountId: this.accountId, 282 | icon: this.icon, 283 | icnsData: this.icnsData, 284 | type: this.type, 285 | keyPair: JSON.stringify(this.identity.toJSON()) 286 | }); 287 | 288 | public burnXTC = async (args: { to: string; amount: string }) => { 289 | if (!validateCanisterId(args.to)) { 290 | throw new Error(ERRORS.INVALID_CANISTER_ID); 291 | } 292 | const xtcActor = await getTokenActor({ 293 | canisterId: TOKENS.XTC.canisterId, 294 | agent: this.agent, 295 | standard: TOKENS.XTC.standard, 296 | }); 297 | const burnResult = await xtcActor.burnXTC({ 298 | to: Principal.fromText(args.to), 299 | amount: args.amount, 300 | }); 301 | try { 302 | if ('Ok' in burnResult) { 303 | const trxId = burnResult.Ok; 304 | await requestCacheUpdate(this.principal, [trxId]); 305 | } 306 | } catch (e) { 307 | console.log('Kyasshu error', e); 308 | } 309 | return burnResult; 310 | }; 311 | 312 | public getTokenBalance = async ({ 313 | token, 314 | }: { 315 | token: StandardToken; 316 | }): Promise => { 317 | try { 318 | const tokenActor = await getTokenActor({ 319 | canisterId: token.canisterId, 320 | agent: this.agent, 321 | standard: token.standard, 322 | }); 323 | 324 | const balance = await tokenActor.getBalance(this.identity.getPrincipal()); 325 | const tokenMetadata = await tokenActor.getMetadata() as FungibleMetadata; 326 | return { 327 | amount: balance.value, 328 | token: { 329 | ...token, 330 | fee: tokenMetadata?.fungible?.fee, 331 | }, 332 | }; 333 | } catch (e) { 334 | console.warn('Get Balance error:', e); 335 | return { 336 | amount: 'Error', 337 | token, 338 | error: e.message, 339 | }; 340 | } 341 | }; 342 | 343 | /* 344 | ** Returns XTC, ICP and WICP balances and all associated registered token balances 345 | ** If any token balance fails to be fetched, it will be flagged with an error 346 | */ 347 | public getBalances = async (): Promise> => { 348 | // Get Custom Token Balances 349 | const walletTokens = this.network.getTokens(this.walletId); 350 | const tokenBalances = await Promise.all(walletTokens.map(token => this.getTokenBalance({ token }))); 351 | return tokenBalances; 352 | }; 353 | 354 | 355 | public getTransactions = async ({icpPrice}): Promise => { 356 | if (this.network.isCustom) return { total: 0, transactions: [] }; 357 | const icnsAdapter = new ICNSAdapter(this.agent); 358 | const [ icpTrxs, xtcTransactions, capTransactions ] = await Promise.all([getICPTransactions(this.accountId), 359 | getXTCTransactions(this.principal), 360 | getCapTransactions({ 361 | principalId: this.principal, 362 | agent: this.agent, 363 | })]) 364 | 365 | let transactionsGroup = [ 366 | ...capTransactions.transactions, 367 | ...icpTrxs.transactions, 368 | ...xtcTransactions.transactions, 369 | ]; 370 | const principals = recursiveFindPrincipals(transactionsGroup); 371 | 372 | const icnsMapping = await icnsAdapter.getICNSMappings(principals); 373 | transactionsGroup = transactionsGroup.map(tx => 374 | replacePrincipalsForICNS(tx, icnsMapping) 375 | ); 376 | const formattedTransactions = formatTransactions(transactionsGroup, this.principal, this.accountId, this.network, icpPrice) 377 | 378 | return formattedTransactions; 379 | }; 380 | 381 | public send = async (args: { 382 | to: string; 383 | amount: string; 384 | canisterId: string; 385 | opts?: TokenInterfaces.SendOpts; 386 | }): Promise => { 387 | const { to, amount, canisterId, opts } = args || {}; 388 | const savedToken = this.network.tokenByCanisterId(canisterId); 389 | if (!savedToken) throw new Error(ERRORS.TOKEN_NOT_REGISTERED); 390 | const tokenActor = await getTokenActor({ 391 | canisterId, 392 | agent: this.agent, 393 | standard: savedToken.standard, 394 | }); 395 | 396 | const result = await tokenActor.send({ 397 | to, 398 | from: this.identity.getPrincipal().toString(), 399 | amount: BigInt(amount), 400 | opts, 401 | }); 402 | if (canisterId === TOKENS.XTC.canisterId) { 403 | try { 404 | if ('transactionId' in result) { 405 | const trxId = result.transactionId; 406 | await requestCacheUpdate(this.principal, [BigInt(trxId)]); 407 | } 408 | } catch (e) { 409 | console.log('Kyasshu error', e); 410 | } 411 | } 412 | 413 | return result; 414 | }; 415 | 416 | public getAgent({ host }: { host?: string }): HttpAgent { 417 | if (host) { 418 | return createAgent({ 419 | defaultIdentity: this.identity, 420 | host, 421 | wrapped: false, 422 | fetch: this.fetch, 423 | }); 424 | } 425 | return this.agent; 426 | } 427 | 428 | public get publicKey(): PublicKey { 429 | return this.identity.getPublicKey(); 430 | } 431 | 432 | public get pemFile(): string { 433 | return this.identity.getPem(); 434 | } 435 | 436 | public getICNSData = async (): Promise<{ 437 | names: string[]; 438 | reverseResolvedName: string | undefined; 439 | }> => { 440 | if (this.network.isCustom) 441 | return { names: [], reverseResolvedName: undefined }; 442 | const icnsAdapter = new ICNSAdapter(this.agent); 443 | const names = await icnsAdapter.getICNSNames(); 444 | const stringNames = names.map((name) => name?.name.toString()); 445 | const reverseResolvedName = await icnsAdapter.getICNSReverseResolvedName(); 446 | this.icnsData = { names: stringNames, reverseResolvedName }; 447 | return { names: stringNames, reverseResolvedName }; 448 | }; 449 | 450 | public getReverseResolvedName = async (): Promise => { 451 | const icnsAdapter = new ICNSAdapter(this.agent); 452 | return icnsAdapter.getICNSReverseResolvedName(); 453 | }; 454 | 455 | public setReverseResolvedName = async ({ 456 | name, 457 | }: { 458 | name: string; 459 | }): Promise => { 460 | const icnsAdapter = new ICNSAdapter(this.agent); 461 | return icnsAdapter.setICNSReverseResolvedName(name); 462 | }; 463 | 464 | public getContacts = async (): Promise> => { 465 | if (this.network.isCustom) return []; 466 | try { 467 | return await getAddresses(this.agent); 468 | } catch (e) { 469 | return []; 470 | } 471 | }; 472 | 473 | public addContact = async ({ 474 | contact, 475 | }: { 476 | contact: Address; 477 | }): Promise => { 478 | try { 479 | if ('PrincipalId' in contact.value) { 480 | const principal = contact.value.PrincipalId; 481 | contact.value = { PrincipalId: Principal.fromText(principal.toString()) }; 482 | } 483 | const contactResponse = await addAddress(this.agent, contact); 484 | return contactResponse.hasOwnProperty('Ok') ? true : false; 485 | } catch (e) { 486 | return false; 487 | } 488 | }; 489 | 490 | public deleteContact = async ({ 491 | addressName, 492 | }: { 493 | addressName: string; 494 | }): Promise => { 495 | try { 496 | const contactResponse = await removeAddress(this.agent, addressName); 497 | 498 | return contactResponse.hasOwnProperty('Ok') ? true : false; 499 | } catch (e) { 500 | return false; 501 | } 502 | }; 503 | 504 | public removeToken = async (args: { 505 | canisterId: string; 506 | }): Promise => { 507 | const { canisterId } = args || {}; 508 | const isDefaultAsset = Object.keys(DEFAULT_MAINNET_ASSETS).includes(canisterId); 509 | 510 | // If it's a default asset, early return 511 | if (isDefaultAsset) return this.network.tokens; 512 | 513 | const tokens = await this.network.removeToken({ 514 | canisterId, 515 | }); 516 | return tokens; 517 | }; 518 | 519 | public delegateIdentity = async (args: { to: Buffer, targets: string[] }): Promise => { 520 | const { to, targets } = args; 521 | const pidTargets = targets.map((target) => Principal.fromText(target)); 522 | const publicKey = Ed25519PublicKey.fromDer(blobFromBuffer(to)); 523 | const delagationChain = await DelegationChain.create( 524 | this.identity, 525 | publicKey, 526 | undefined, // Expiration arg, default to 15 mins 527 | { targets: pidTargets } 528 | ); 529 | return JSON.stringify(delagationChain.toJSON()); 530 | }; 531 | } 532 | 533 | export default PlugWallet; 534 | -------------------------------------------------------------------------------- /src/constants/tokens.ts: -------------------------------------------------------------------------------- 1 | import { standards } from '@psychedelic/dab-js'; 2 | 3 | import { LEDGER_CANISTER_ID } from '../utils/dfx/constants'; 4 | 5 | export const TOKENS = { 6 | ICP: { 7 | symbol: 'ICP', 8 | canisterId: LEDGER_CANISTER_ID, 9 | name: 'ICP', 10 | decimals: 8, 11 | standard: standards.TOKEN.rosetta, 12 | }, 13 | XTC: { 14 | symbol: 'XTC', 15 | canisterId: 'aanaa-xaaaa-aaaah-aaeiq-cai', 16 | name: 'Cycles', 17 | decimals: 12, 18 | standard: standards.TOKEN.xtc, 19 | }, 20 | WTC: { 21 | symbol: 'WTC', 22 | canisterId: '5ymop-yyaaa-aaaah-qaa4q-cai', 23 | name: 'Wrapped Cycles', 24 | decimals: 12, 25 | standard: standards.TOKEN.dip20, 26 | }, 27 | WICP: { 28 | symbol: 'WICP', 29 | canisterId: 'utozz-siaaa-aaaam-qaaxq-cai', 30 | name: 'Wrapped ICP', 31 | decimals: 8, 32 | standard: standards.TOKEN.wicp, 33 | }, 34 | BTKN: { 35 | symbol: 'BTKN', 36 | canisterId: 'cfoim-fqaaa-aaaai-qbcmq-cai', 37 | name: 'Beta Token', 38 | decimals: 8, 39 | standard: standards.TOKEN.dip20, 40 | }, 41 | DUST: { 42 | symbol: 'DUST', 43 | canisterId: '4mvfv-piaaa-aaaak-aacia-cai', 44 | name: 'Dust Token', 45 | decimals: 8, 46 | standard: standards.TOKEN.dip20, 47 | }, 48 | }; 49 | 50 | export const DEFAULT_MAINNET_TOKENS = [TOKENS.ICP, TOKENS.XTC, TOKENS.WICP]; 51 | 52 | export const DEFAULT_MAINNET_ASSETS = DEFAULT_MAINNET_TOKENS.reduce( 53 | (acum, token) => ({ ...acum, [token.canisterId]: { token, amount: '0' } }), 54 | {} 55 | ); 56 | -------------------------------------------------------------------------------- /src/constants/version.ts: -------------------------------------------------------------------------------- 1 | export const PLUG_CONTROLLER_VERSION = "0.25.3"; 2 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | export const ERRORS = { 3 | INVALID_MNEMONIC: 'The provided mnemonic is invalid', 4 | INVALID_ACC_ID: 'The account ID should be a positive integer', 5 | PASSWORD_REQUIRED: 'A password is required', 6 | NOT_INITIALIZED: 'Plug must be initialized', 7 | STATE_LOCKED: 'The state is locked', 8 | INVALID_WALLET_NUMBER: 'Invalid wallet number', 9 | GET_TRANSACTIONS_FAILS: 'Get transactions fails', 10 | INVALID_CANISTER_ID: 'The provided canister id is invalid', 11 | TOKEN_NOT_SUPPORTED: 12 | 'The provided canister does not implement common extensions from EXT token interface. Please refer to "https://github.com/Toniq-Labs/extendable-token" for further information.', 13 | NON_FUNGIBLE_TOKEN_NOT_SUPPORTED: 'Non fungible tokens are not supported yet', 14 | TOKEN_NOT_SUPPORT_METADATA: 15 | 'The provided canister does not implement commont extension', 16 | INVALID_PRINCIPAL_ID: 'Invalid principal id', 17 | GET_NFT_ERROR: 'Error while fetching NFT data', 18 | TRANSFER_NFT_ERROR: 19 | 'Error while trying to transfer the NFT.\n Please verify that the NFT you are trying to transfer is not locked or listed for sale', 20 | INVALID_APP: 'Invalid app', 21 | ICNS_REVERSE_RESOLVER_ERROR: 'Error while interacting with ICNS reverse resolver', 22 | ICNS_REGISTRY_ERROR: 'Error while interacting with ICNS registry', 23 | ICNS_RESOLVER_ERROR: 'Error while interacting with ICNS resolver', 24 | INVALID_NETWORK_ID: 'Invalid network id', 25 | EMPTY_IDENTITY_ERROR: 'Identity or SecretKey missing', 26 | INVALID_TYPE_ERROR: 'No such type allowed', 27 | DELETE_ACCOUNT_ERROR: 'Only imported accounts could be deleted', 28 | TOKEN_NOT_REGISTERED: 'Token not registered', 29 | INVALID_ACCOUNT: 'The imported account already exists', 30 | NFT_ALREADY_REGISTERED: 'The NFT is already registered', 31 | CANISTER_INTERFACE_ERROR: 'Canister does not implement the selected standard interface', 32 | }; 33 | 34 | export const ERROR_CODES = { 35 | ADDED_ACCOUNT: 'added-account', 36 | INVALID_KEY: 'invalid-key', 37 | }; 38 | 39 | -------------------------------------------------------------------------------- /src/idls/canister.did.ts: -------------------------------------------------------------------------------- 1 | export default ({ IDL }: { IDL: any }) => { 2 | return IDL.Service({ 3 | greet: IDL.Func([IDL.Text], [IDL.Text], []), 4 | http_request: IDL.Func([], [IDL.Text], []), 5 | }); 6 | }; 7 | -------------------------------------------------------------------------------- /src/idls/icns_registry.did.ts: -------------------------------------------------------------------------------- 1 | export default ({ IDL }) => { 2 | const TxReceipt = IDL.Variant({ ok: IDL.Nat, err: IDL.Text }); 3 | const Time = IDL.Int; 4 | const RecordExt = IDL.Record({ 5 | ttl: IDL.Nat64, 6 | controller: IDL.Principal, 7 | resolver: IDL.Principal, 8 | owner: IDL.Principal, 9 | operator: IDL.Principal, 10 | name: IDL.Text, 11 | expiry: Time, 12 | id: IDL.Nat, 13 | }); 14 | const Info = IDL.Record({ 15 | memSize: IDL.Nat, 16 | heapSize: IDL.Nat, 17 | historySize: IDL.Nat, 18 | cycles: IDL.Nat, 19 | names: IDL.Nat, 20 | users: IDL.Nat, 21 | }); 22 | const ICNSRegistry = IDL.Service({ 23 | addWhitelist: IDL.Func([IDL.Text], [IDL.Bool], []), 24 | approve: IDL.Func([IDL.Text, IDL.Principal], [TxReceipt], []), 25 | balanceOf: IDL.Func([IDL.Principal], [IDL.Nat], ['query']), 26 | controller: IDL.Func([IDL.Text], [IDL.Opt(IDL.Principal)], ['query']), 27 | expiry: IDL.Func([IDL.Text], [IDL.Opt(Time)], ['query']), 28 | exportOwnerDomains: IDL.Func( 29 | [], 30 | [IDL.Vec(IDL.Tuple(IDL.Principal, IDL.Vec(IDL.Text)))], 31 | ['query'], 32 | ), 33 | getApproved: IDL.Func([IDL.Text], [IDL.Opt(IDL.Principal)], ['query']), 34 | getControllerDomains: IDL.Func( 35 | [IDL.Principal], 36 | [IDL.Opt(IDL.Vec(RecordExt))], 37 | ['query'], 38 | ), 39 | getInfo: IDL.Func([], [Info], ['query']), 40 | getRecord: IDL.Func([IDL.Text], [IDL.Opt(RecordExt)], ['query']), 41 | getUserDomains: IDL.Func( 42 | [IDL.Principal], 43 | [IDL.Opt(IDL.Vec(RecordExt))], 44 | ['query'], 45 | ), 46 | isApproved: IDL.Func([IDL.Text, IDL.Principal], [IDL.Bool], ['query']), 47 | isApprovedForAll: IDL.Func( 48 | [IDL.Principal, IDL.Principal], 49 | [IDL.Bool], 50 | ['query'], 51 | ), 52 | isWhitelisted: IDL.Func([IDL.Text], [IDL.Bool], []), 53 | owner: IDL.Func([IDL.Text], [IDL.Opt(IDL.Principal)], ['query']), 54 | recordExists: IDL.Func([IDL.Text], [IDL.Bool], ['query']), 55 | removeWhitelist: IDL.Func([IDL.Text], [IDL.Bool], []), 56 | resolver: IDL.Func([IDL.Text], [IDL.Opt(IDL.Principal)], ['query']), 57 | setApprovalForAll: IDL.Func([IDL.Principal, IDL.Bool], [TxReceipt], []), 58 | setController: IDL.Func([IDL.Text, IDL.Principal], [TxReceipt], []), 59 | setOwner: IDL.Func([IDL.Text, IDL.Principal], [TxReceipt], []), 60 | setRecord: IDL.Func( 61 | [IDL.Text, IDL.Principal, IDL.Principal, IDL.Nat64, Time], 62 | [TxReceipt], 63 | [], 64 | ), 65 | setResolver: IDL.Func([IDL.Text, IDL.Principal], [TxReceipt], []), 66 | setSubnodeExpiry: IDL.Func([IDL.Text, IDL.Text, Time], [TxReceipt], []), 67 | setSubnodeOwner: IDL.Func( 68 | [IDL.Text, IDL.Text, IDL.Principal], 69 | [TxReceipt], 70 | [], 71 | ), 72 | setSubnodeRecord: IDL.Func( 73 | [IDL.Text, IDL.Text, IDL.Principal, IDL.Principal, IDL.Nat64, Time], 74 | [TxReceipt], 75 | [], 76 | ), 77 | setTTL: IDL.Func([IDL.Text, IDL.Nat64], [TxReceipt], []), 78 | transfer: IDL.Func([IDL.Principal, IDL.Text], [TxReceipt], []), 79 | transferFrom: IDL.Func( 80 | [IDL.Principal, IDL.Principal, IDL.Text], 81 | [TxReceipt], 82 | [], 83 | ), 84 | ttl: IDL.Func([IDL.Text], [IDL.Opt(IDL.Nat64)], ['query']), 85 | }); 86 | return ICNSRegistry; 87 | }; 88 | export const init = ({ IDL }) => [IDL.Principal, IDL.Principal]; 89 | -------------------------------------------------------------------------------- /src/idls/icns_resolver.did.ts: -------------------------------------------------------------------------------- 1 | export default ({ IDL }) => { 2 | const Info = IDL.Record({ 3 | extensionLimit: IDL.Nat, 4 | memSize: IDL.Nat, 5 | heapSize: IDL.Nat, 6 | maxRecordLength: IDL.Nat, 7 | entries: IDL.Nat, 8 | cycles: IDL.Nat, 9 | }); 10 | const DefaultInfoExt = IDL.Record({ 11 | btc: IDL.Opt(IDL.Text), 12 | eth: IDL.Opt(IDL.Text), 13 | icp: IDL.Opt(IDL.Text), 14 | pid: IDL.Opt(IDL.Principal), 15 | url: IDL.Opt(IDL.Text), 16 | twitter: IDL.Opt(IDL.Text), 17 | host: IDL.Opt( 18 | IDL.Variant({ url: IDL.Text, canister: IDL.Principal }), 19 | ), 20 | canisterExtensions: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Principal)), 21 | description: IDL.Opt(IDL.Text), 22 | email: IDL.Opt(IDL.Text), 23 | textExtensions: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), 24 | addrExtensions: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), 25 | discord: IDL.Opt(IDL.Text), 26 | mainCanister: IDL.Opt(IDL.Principal), 27 | telegram: IDL.Opt(IDL.Text), 28 | github: IDL.Opt(IDL.Text), 29 | avatar: IDL.Opt(IDL.Text), 30 | }); 31 | const Result = IDL.Variant({ ok: IDL.Null, err: IDL.Text }); 32 | const ICNSResolver = IDL.Service({ 33 | getAddr: IDL.Func([IDL.Text, IDL.Text], [IDL.Opt(IDL.Text)], []), 34 | getCanister: IDL.Func( 35 | [IDL.Text, IDL.Text], 36 | [IDL.Opt(IDL.Principal)], 37 | [], 38 | ), 39 | getExtensionLimit: IDL.Func([], [IDL.Nat], ['query']), 40 | getHost: IDL.Func( 41 | [IDL.Text], 42 | [ 43 | IDL.Opt( 44 | IDL.Variant({ url: IDL.Text, canister: IDL.Principal }), 45 | ), 46 | ], 47 | ['query'], 48 | ), 49 | getInfo: IDL.Func([], [Info], ['query']), 50 | getLengthLimit: IDL.Func([], [IDL.Nat], ['query']), 51 | getText: IDL.Func([IDL.Text, IDL.Text], [IDL.Opt(IDL.Text)], []), 52 | getUserDefaultInfo: IDL.Func( 53 | [IDL.Text], 54 | [IDL.Opt(DefaultInfoExt)], 55 | ['query'], 56 | ), 57 | setAddr: IDL.Func([IDL.Text, IDL.Text, IDL.Opt(IDL.Text)], [Result], []), 58 | setCanister: IDL.Func( 59 | [IDL.Text, IDL.Text, IDL.Opt(IDL.Principal)], 60 | [Result], 61 | [], 62 | ), 63 | setExtensionLimit: IDL.Func([IDL.Nat], [IDL.Nat], []), 64 | setHost: IDL.Func( 65 | [ 66 | IDL.Text, 67 | IDL.Opt( 68 | IDL.Variant({ url: IDL.Text, canister: IDL.Principal }), 69 | ), 70 | ], 71 | [Result], 72 | [], 73 | ), 74 | setLengthLimit: IDL.Func([IDL.Nat], [IDL.Nat], []), 75 | setText: IDL.Func([IDL.Text, IDL.Text, IDL.Opt(IDL.Text)], [Result], []), 76 | }); 77 | return ICNSResolver; 78 | }; 79 | export const init = ({ IDL }) => [IDL.Principal, IDL.Principal]; 80 | -------------------------------------------------------------------------------- /src/idls/icns_reverse_registrar.did.ts: -------------------------------------------------------------------------------- 1 | export default ({ IDL }) => { 2 | const Result = IDL.Variant({ 'ok' : IDL.Text, 'err' : IDL.Text }); 3 | const ICNSReverseRegistrar = IDL.Service({ 4 | 'getName' : IDL.Func([IDL.Principal], [IDL.Text], ['query']), 5 | 'setName' : IDL.Func([IDL.Text], [Result], []), 6 | }); 7 | return ICNSReverseRegistrar; 8 | }; 9 | export const init = ({ IDL }) => { 10 | return [IDL.Principal, IDL.Principal, IDL.Text]; 11 | }; -------------------------------------------------------------------------------- /src/idls/ledger.did.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/class-name-casing */ 2 | /* eslint-disable camelcase */ 3 | /* eslint-disable @typescript-eslint/camelcase */ 4 | export default ({ IDL }) => { 5 | const AccountIdentifier = IDL.Text; 6 | const Duration = IDL.Record({ secs: IDL.Nat64, nanos: IDL.Nat32 }); 7 | const ArchiveOptions = IDL.Record({ 8 | max_message_size_bytes: IDL.Opt(IDL.Nat32), 9 | node_max_memory_size_bytes: IDL.Opt(IDL.Nat32), 10 | controller_id: IDL.Principal, 11 | }); 12 | const ICPTs = IDL.Record({ e8s: IDL.Nat64 }); 13 | const LedgerCanisterInitPayload = IDL.Record({ 14 | send_whitelist: IDL.Vec(IDL.Tuple(IDL.Principal)), 15 | minting_account: AccountIdentifier, 16 | transaction_window: IDL.Opt(Duration), 17 | max_message_size_bytes: IDL.Opt(IDL.Nat32), 18 | archive_options: IDL.Opt(ArchiveOptions), 19 | initial_values: IDL.Vec(IDL.Tuple(AccountIdentifier, ICPTs)), 20 | }); 21 | const AccountBalanceArgs = IDL.Record({ account: AccountIdentifier }); 22 | const SubAccount = IDL.Vec(IDL.Nat8); 23 | const BlockHeight = IDL.Nat64; 24 | const NotifyCanisterArgs = IDL.Record({ 25 | to_subaccount: IDL.Opt(SubAccount), 26 | from_subaccount: IDL.Opt(SubAccount), 27 | to_canister: IDL.Principal, 28 | max_fee: ICPTs, 29 | block_height: BlockHeight, 30 | }); 31 | const Memo = IDL.Nat64; 32 | const TimeStamp = IDL.Record({ timestamp_nanos: IDL.Nat64 }); 33 | const SendArgs = IDL.Record({ 34 | to: AccountIdentifier, 35 | fee: ICPTs, 36 | memo: Memo, 37 | from_subaccount: IDL.Opt(SubAccount), 38 | created_at_time: IDL.Opt(TimeStamp), 39 | amount: ICPTs, 40 | }); 41 | return IDL.Service({ 42 | account_balance_dfx: IDL.Func([AccountBalanceArgs], [ICPTs], ['query']), 43 | notify_dfx: IDL.Func([NotifyCanisterArgs], [], []), 44 | send_dfx: IDL.Func([SendArgs], [BlockHeight], []), 45 | }); 46 | }; 47 | export const init = ({ IDL }) => { 48 | const AccountIdentifier = IDL.Text; 49 | const Duration = IDL.Record({ secs: IDL.Nat64, nanos: IDL.Nat32 }); 50 | const ArchiveOptions = IDL.Record({ 51 | max_message_size_bytes: IDL.Opt(IDL.Nat32), 52 | node_max_memory_size_bytes: IDL.Opt(IDL.Nat32), 53 | controller_id: IDL.Principal, 54 | }); 55 | const ICPTs = IDL.Record({ e8s: IDL.Nat64 }); 56 | const LedgerCanisterInitPayload = IDL.Record({ 57 | send_whitelist: IDL.Vec(IDL.Tuple(IDL.Principal)), 58 | minting_account: AccountIdentifier, 59 | transaction_window: IDL.Opt(Duration), 60 | max_message_size_bytes: IDL.Opt(IDL.Nat32), 61 | archive_options: IDL.Opt(ArchiveOptions), 62 | initial_values: IDL.Vec(IDL.Tuple(AccountIdentifier, ICPTs)), 63 | }); 64 | return [LedgerCanisterInitPayload]; 65 | }; 66 | -------------------------------------------------------------------------------- /src/idls/nns_uid.did.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/class-name-casing */ 2 | /* eslint-disable camelcase */ 3 | /* eslint-disable @typescript-eslint/camelcase */ 4 | export default ({ IDL }) => { 5 | const AccountIdentifier = IDL.Text; 6 | const AttachCanisterRequest = IDL.Record({ 7 | name: IDL.Text, 8 | canister_id: IDL.Principal, 9 | }); 10 | const AttachCanisterResponse = IDL.Variant({ 11 | Ok: IDL.Null, 12 | CanisterAlreadyAttached: IDL.Null, 13 | NameAlreadyTaken: IDL.Null, 14 | NameTooLong: IDL.Null, 15 | CanisterLimitExceeded: IDL.Null, 16 | }); 17 | const SubAccount = IDL.Vec(IDL.Nat8); 18 | const SubAccountDetails = IDL.Record({ 19 | name: IDL.Text, 20 | sub_account: SubAccount, 21 | account_identifier: AccountIdentifier, 22 | }); 23 | const CreateSubAccountResponse = IDL.Variant({ 24 | Ok: SubAccountDetails, 25 | AccountNotFound: IDL.Null, 26 | NameTooLong: IDL.Null, 27 | SubAccountLimitExceeded: IDL.Null, 28 | }); 29 | const DetachCanisterRequest = IDL.Record({ canister_id: IDL.Principal }); 30 | const DetachCanisterResponse = IDL.Variant({ 31 | Ok: IDL.Null, 32 | CanisterNotFound: IDL.Null, 33 | }); 34 | const HardwareWalletAccountDetails = IDL.Record({ 35 | name: IDL.Text, 36 | account_identifier: AccountIdentifier, 37 | }); 38 | const AccountDetails = IDL.Record({ 39 | account_identifier: AccountIdentifier, 40 | hardware_wallet_accounts: IDL.Vec(HardwareWalletAccountDetails), 41 | sub_accounts: IDL.Vec(SubAccountDetails), 42 | }); 43 | const GetAccountResponse = IDL.Variant({ 44 | Ok: AccountDetails, 45 | AccountNotFound: IDL.Null, 46 | }); 47 | const CanisterDetails = IDL.Record({ 48 | name: IDL.Text, 49 | canister_id: IDL.Principal, 50 | }); 51 | const BlockHeight = IDL.Nat64; 52 | const Stats = IDL.Record({ 53 | latest_transaction_block_height: BlockHeight, 54 | seconds_since_last_ledger_sync: IDL.Nat64, 55 | sub_accounts_count: IDL.Nat64, 56 | hardware_wallet_accounts_count: IDL.Nat64, 57 | accounts_count: IDL.Nat64, 58 | earliest_transaction_block_height: BlockHeight, 59 | transactions_count: IDL.Nat64, 60 | block_height_synced_up_to: IDL.Opt(IDL.Nat64), 61 | latest_transaction_timestamp_nanos: IDL.Nat64, 62 | earliest_transaction_timestamp_nanos: IDL.Nat64, 63 | }); 64 | const GetTransactionsRequest = IDL.Record({ 65 | page_size: IDL.Nat8, 66 | offset: IDL.Nat32, 67 | account_identifier: AccountIdentifier, 68 | }); 69 | const Timestamp = IDL.Record({ timestamp_nanos: IDL.Nat64 }); 70 | const ICPTs = IDL.Record({ e8s: IDL.Nat64 }); 71 | const Send = IDL.Record({ 72 | to: AccountIdentifier, 73 | fee: ICPTs, 74 | amount: ICPTs, 75 | }); 76 | const Receive = IDL.Record({ 77 | fee: ICPTs, 78 | from: AccountIdentifier, 79 | amount: ICPTs, 80 | }); 81 | const Transfer = IDL.Variant({ 82 | Burn: IDL.Record({ amount: ICPTs }), 83 | Mint: IDL.Record({ amount: ICPTs }), 84 | Send, 85 | Receive, 86 | }); 87 | const Transaction = IDL.Record({ 88 | memo: IDL.Nat64, 89 | timestamp: Timestamp, 90 | block_height: BlockHeight, 91 | transfer: Transfer, 92 | }); 93 | const GetTransactionsResponse = IDL.Record({ 94 | total: IDL.Nat32, 95 | transactions: IDL.Vec(Transaction), 96 | }); 97 | const HeaderField = IDL.Tuple(IDL.Text, IDL.Text); 98 | const HttpRequest = IDL.Record({ 99 | url: IDL.Text, 100 | method: IDL.Text, 101 | body: IDL.Vec(IDL.Nat8), 102 | headers: IDL.Vec(HeaderField), 103 | }); 104 | const HttpResponse = IDL.Record({ 105 | body: IDL.Vec(IDL.Nat8), 106 | headers: IDL.Vec(HeaderField), 107 | status_code: IDL.Nat16, 108 | }); 109 | const RegisterHardwareWalletRequest = IDL.Record({ 110 | name: IDL.Text, 111 | account_identifier: AccountIdentifier, 112 | }); 113 | const RegisterHardwareWalletResponse = IDL.Variant({ 114 | Ok: IDL.Null, 115 | AccountNotFound: IDL.Null, 116 | HardwareWalletAlreadyRegistered: IDL.Null, 117 | HardwareWalletLimitExceeded: IDL.Null, 118 | NameTooLong: IDL.Null, 119 | }); 120 | const RemoveHardwareWalletRequest = IDL.Record({ 121 | account_identifier: AccountIdentifier, 122 | }); 123 | const RemoveHardwareWalletResponse = IDL.Variant({ 124 | Ok: IDL.Null, 125 | HardwareWalletNotFound: IDL.Null, 126 | }); 127 | const RenameSubAccountRequest = IDL.Record({ 128 | new_name: IDL.Text, 129 | account_identifier: AccountIdentifier, 130 | }); 131 | const RenameSubAccountResponse = IDL.Variant({ 132 | Ok: IDL.Null, 133 | AccountNotFound: IDL.Null, 134 | SubAccountNotFound: IDL.Null, 135 | NameTooLong: IDL.Null, 136 | }); 137 | return IDL.Service({ 138 | add_account: IDL.Func([], [AccountIdentifier], []), 139 | attach_canister: IDL.Func( 140 | [AttachCanisterRequest], 141 | [AttachCanisterResponse], 142 | [] 143 | ), 144 | create_sub_account: IDL.Func([IDL.Text], [CreateSubAccountResponse], []), 145 | detach_canister: IDL.Func( 146 | [DetachCanisterRequest], 147 | [DetachCanisterResponse], 148 | [] 149 | ), 150 | get_account: IDL.Func([], [GetAccountResponse], ['query']), 151 | get_canisters: IDL.Func([], [IDL.Vec(CanisterDetails)], ['query']), 152 | get_icp_to_cycles_conversion_rate: IDL.Func([], [IDL.Nat64], ['query']), 153 | get_stats: IDL.Func([], [Stats], ['query']), 154 | get_transactions: IDL.Func( 155 | [GetTransactionsRequest], 156 | [GetTransactionsResponse], 157 | ['query'] 158 | ), 159 | http_request: IDL.Func([HttpRequest], [HttpResponse], ['query']), 160 | register_hardware_wallet: IDL.Func( 161 | [RegisterHardwareWalletRequest], 162 | [RegisterHardwareWalletResponse], 163 | [] 164 | ), 165 | remove_hardware_wallet: IDL.Func( 166 | [RemoveHardwareWalletRequest], 167 | [RemoveHardwareWalletResponse], 168 | [] 169 | ), 170 | rename_sub_account: IDL.Func( 171 | [RenameSubAccountRequest], 172 | [RenameSubAccountResponse], 173 | [] 174 | ), 175 | }); 176 | }; 177 | export const init = _arg => { 178 | return []; 179 | }; 180 | -------------------------------------------------------------------------------- /src/idls/walltet.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/class-name-casing */ 2 | /* eslint-disable camelcase */ 3 | /* eslint-disable @typescript-eslint/camelcase */ 4 | import { IDL } from '@dfinity/candid'; 5 | 6 | const walletIDLFactory: IDL.InterfaceFactory = ({ IDL }) => { 7 | const Kind = IDL.Variant({ 8 | User: IDL.Null, 9 | Canister: IDL.Null, 10 | Unknown: IDL.Null, 11 | }); 12 | const Role = IDL.Variant({ 13 | Custodian: IDL.Null, 14 | Contact: IDL.Null, 15 | Controller: IDL.Null, 16 | }); 17 | const AddressEntry = IDL.Record({ 18 | id: IDL.Principal, 19 | kind: Kind, 20 | name: IDL.Opt(IDL.Text), 21 | role: Role, 22 | }); 23 | const EventKind = IDL.Variant({ 24 | CyclesReceived: IDL.Record({ 25 | from: IDL.Principal, 26 | amount: IDL.Nat64, 27 | }), 28 | CanisterCreated: IDL.Record({ 29 | cycles: IDL.Nat64, 30 | canister: IDL.Principal, 31 | }), 32 | CanisterCalled: IDL.Record({ 33 | cycles: IDL.Nat64, 34 | method_name: IDL.Text, 35 | canister: IDL.Principal, 36 | }), 37 | CyclesSent: IDL.Record({ 38 | to: IDL.Principal, 39 | amount: IDL.Nat64, 40 | refund: IDL.Nat64, 41 | }), 42 | AddressRemoved: IDL.Record({ id: IDL.Principal }), 43 | WalletDeployed: IDL.Record({ canister: IDL.Principal }), 44 | AddressAdded: IDL.Record({ 45 | id: IDL.Principal, 46 | name: IDL.Opt(IDL.Text), 47 | role: Role, 48 | }), 49 | }); 50 | const Event = IDL.Record({ 51 | id: IDL.Nat32, 52 | kind: EventKind, 53 | timestamp: IDL.Nat64, 54 | }); 55 | const ResultCall = IDL.Variant({ 56 | Ok: IDL.Record({ return: IDL.Vec(IDL.Nat8) }), 57 | Err: IDL.Text, 58 | }); 59 | const CanisterSettings = IDL.Record({ 60 | controller: IDL.Opt(IDL.Principal), 61 | freezing_threshold: IDL.Opt(IDL.Nat), 62 | memory_allocation: IDL.Opt(IDL.Nat), 63 | compute_allocation: IDL.Opt(IDL.Nat), 64 | }); 65 | const CreateCanisterArgs = IDL.Record({ 66 | cycles: IDL.Nat64, 67 | settings: CanisterSettings, 68 | }); 69 | const ResultCreate = IDL.Variant({ 70 | Ok: IDL.Record({ canister_id: IDL.Principal }), 71 | Err: IDL.Text, 72 | }); 73 | const ResultSend = IDL.Variant({ Ok: IDL.Null, Err: IDL.Text }); 74 | return IDL.Service({ 75 | add_address: IDL.Func([AddressEntry], [], []), 76 | add_controller: IDL.Func([IDL.Principal], [], []), 77 | authorize: IDL.Func([IDL.Principal], [], []), 78 | deauthorize: IDL.Func([IDL.Principal], [], []), 79 | get_chart: IDL.Func( 80 | [ 81 | IDL.Opt( 82 | IDL.Record({ 83 | count: IDL.Opt(IDL.Nat32), 84 | precision: IDL.Opt(IDL.Nat64), 85 | }) 86 | ), 87 | ], 88 | [IDL.Vec(IDL.Tuple(IDL.Nat64, IDL.Nat64))], 89 | ['query'] 90 | ), 91 | get_controllers: IDL.Func([], [IDL.Vec(IDL.Principal)], ['query']), 92 | get_custodians: IDL.Func([], [IDL.Vec(IDL.Principal)], ['query']), 93 | get_events: IDL.Func( 94 | [ 95 | IDL.Opt( 96 | IDL.Record({ 97 | to: IDL.Opt(IDL.Nat32), 98 | from: IDL.Opt(IDL.Nat32), 99 | }) 100 | ), 101 | ], 102 | [IDL.Vec(Event)], 103 | ['query'] 104 | ), 105 | list_addresses: IDL.Func([], [IDL.Vec(AddressEntry)], ['query']), 106 | name: IDL.Func([], [IDL.Opt(IDL.Text)], ['query']), 107 | remove_address: IDL.Func([IDL.Principal], [], []), 108 | remove_controller: IDL.Func([IDL.Principal], [], []), 109 | set_name: IDL.Func([IDL.Text], [], []), 110 | wallet_balance: IDL.Func( 111 | [], 112 | [IDL.Record({ amount: IDL.Nat64 })], 113 | ['query'] 114 | ), 115 | wallet_call: IDL.Func( 116 | [ 117 | IDL.Record({ 118 | args: IDL.Vec(IDL.Nat8), 119 | cycles: IDL.Nat64, 120 | method_name: IDL.Text, 121 | canister: IDL.Principal, 122 | }), 123 | ], 124 | [ResultCall], 125 | [] 126 | ), 127 | wallet_create_canister: IDL.Func([CreateCanisterArgs], [ResultCreate], []), 128 | wallet_create_wallet: IDL.Func([CreateCanisterArgs], [ResultCreate], []), 129 | wallet_receive: IDL.Func([], [], []), 130 | wallet_send: IDL.Func( 131 | [IDL.Record({ canister: IDL.Principal, amount: IDL.Nat64 })], 132 | [ResultSend], 133 | [] 134 | ), 135 | wallet_store_wallet_wasm: IDL.Func( 136 | [IDL.Record({ wasm_module: IDL.Vec(IDL.Nat8) })], 137 | [], 138 | [] 139 | ), 140 | }); 141 | }; 142 | 143 | export default walletIDLFactory; 144 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import PlugKeyRing from './PlugKeyRing'; 2 | import { getAccountId } from './utils/account'; 3 | import { getCanisterInfo, getMultipleCanisterInfo } from './utils/dab'; 4 | import { decode, encode } from './utils/idl'; 5 | 6 | export default { 7 | PlugKeyRing, 8 | getAccountId, 9 | getCanisterInfo, 10 | getMultipleCanisterInfo, 11 | IDLDecode: decode, 12 | IDLEncode: encode, 13 | }; 14 | -------------------------------------------------------------------------------- /src/interfaces/account.ts: -------------------------------------------------------------------------------- 1 | import Secp256k1KeyIdentity from '../utils/identity/secpk256k1/identity'; 2 | 3 | export interface AccountCredentials { 4 | mnemonic: string; 5 | identity: Secp256k1KeyIdentity; 6 | accountId: string; 7 | } 8 | 9 | export interface ConnectedApp { 10 | name: string; 11 | icon: string; 12 | url: string; 13 | whitelist: Array; 14 | } 15 | -------------------------------------------------------------------------------- /src/interfaces/cap.ts: -------------------------------------------------------------------------------- 1 | import { HttpAgent } from '@dfinity/agent'; 2 | import { Principal } from '@dfinity/principal'; 3 | import crossFetch from 'cross-fetch'; 4 | import { InferredTransaction } from './transactions'; 5 | 6 | export interface LastEvaluatedKey { 7 | pk: string; 8 | sk: string; 9 | userId: string; 10 | } 11 | 12 | export interface GetUserTransactionResponse { 13 | total: number; 14 | transactions: InferredTransaction[]; 15 | lastEvaluatedKey?: LastEvaluatedKey; 16 | } 17 | 18 | export interface GetCapTransactionsParams { 19 | principalId: string; 20 | lastEvaluatedKey?: LastEvaluatedKey; 21 | agent?: HttpAgent; 22 | } 23 | 24 | export interface KyashuItem { 25 | contractId: string; 26 | event: any; 27 | pk: string; 28 | sk: string; 29 | userId: string; 30 | gs1sk: string; 31 | gs1pk: string; 32 | caller: Principal; 33 | } 34 | -------------------------------------------------------------------------------- /src/interfaces/contact_registry.ts: -------------------------------------------------------------------------------- 1 | import { Principal } from '@dfinity/principal'; 2 | 3 | export type ValueType = { 'PrincipalId': Principal } | { 'AccountId': string } | { 'Icns': string } 4 | 5 | export interface Address { 6 | 'name': string, 7 | 'description': [] | [string], 8 | 'emoji': [] | [string], 9 | 'value': ValueType, 10 | } 11 | 12 | export type Error = { 'NotAuthorized' : null } | 13 | { 'BadParameters' : null } | 14 | { 'Unknown' : string } | 15 | { 'NonExistentItem' : null }; 16 | 17 | export type Response = { 'Ok' : [] | [string] } | 18 | { 'Err' : Error }; 19 | -------------------------------------------------------------------------------- /src/interfaces/dank.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/class-name-casing */ 2 | /* eslint-disable camelcase */ 3 | /* eslint-disable @typescript-eslint/camelcase */ 4 | import type { Principal } from '@dfinity/principal'; 5 | 6 | export interface AllowanceRule { 7 | 'max_portion' : [] | [bigint], 8 | 'max_allowed_calls_per_day' : [] | [number], 9 | 'stop_threshold' : [] | [bigint], 10 | 'max_cycles' : [] | [bigint], 11 | } 12 | export interface Event { 13 | 'fee' : bigint, 14 | 'status' : TransactionStatus, 15 | 'detail' : EventDetail, 16 | 'timestamp' : bigint, 17 | 'amount' : bigint, 18 | } 19 | export type EventDetail = { 20 | 'Withdraw' : { 'to' : Principal, 'from' : Principal } 21 | } | 22 | { 'ChargingStationDeployed' : { 'canister' : Principal } } | 23 | { 'Deposit' : { 'to' : Principal } } | 24 | { 'CanisterCreated' : { 'canister' : Principal } } | 25 | { 'CanisterCalled' : { 'method_name' : string, 'canister' : Principal } } | 26 | { 'Transfer' : { 'to' : Principal, 'from' : Principal } }; 27 | export interface EventsConnection { 28 | 'data' : Array, 29 | 'next_canister_id' : [] | [Principal], 30 | } 31 | export type GetTransactionResult = { 'None' : null } | 32 | { 'Some' : Event }; 33 | export type ResultCall = { 'Ok' : { 'return' : Array } } | 34 | { 'Err' : string }; 35 | export type TransactionId = bigint; 36 | export type TransactionStatus = { 'InternalError' : null } | 37 | { 'Completed' : null } | 38 | { 'Pending' : null } | 39 | { 'InsufficientFunds' : null }; 40 | export type TransferError = { 'InsufficientBalance' : null } | 41 | { 'InternalError' : string } | 42 | { 'AmountTooLarge' : null }; 43 | export type TransferResponse = { 'Ok' : TransactionId } | 44 | { 'Err' : TransferError }; 45 | export type WithdrawError = { 'InsufficientBalance' : null } | 46 | { 'InternalError' : string } | 47 | { 'AmountTooLarge' : null }; 48 | export type WithdrawResult = { 'Ok' : TransactionId } | 49 | { 'Err' : WithdrawError }; 50 | export default interface _SERVICE { 51 | 'allow' : ( 52 | arg_0: { 'rule' : AllowanceRule, 'canister_id' : Principal }, 53 | ) => Promise, 54 | 'balance' : (arg_0: [] | [Principal]) => Promise, 55 | 'deposit' : (arg_0: [] | [Principal]) => Promise, 56 | 'disallow' : (arg_0: Principal) => Promise, 57 | 'events' : (arg_0: { 'after' : [] | [number], 'limit' : number }) => Promise< 58 | EventsConnection 59 | >, 60 | 'get_transaction' : () => Promise, 61 | 'name' : () => Promise, 62 | 'request_withdraw' : () => Promise, 63 | 'transfer' : (arg_0: { 'to' : Principal, 'amount' : bigint }) => Promise< 64 | TransferResponse 65 | >, 66 | 'wallet_call' : ( 67 | arg_0: { 68 | 'args' : Array, 69 | 'cycles' : bigint, 70 | 'method_name' : string, 71 | 'canister' : Principal, 72 | }, 73 | ) => Promise, 74 | 'withdraw' : ( 75 | arg_0: { 'canister_id' : Principal, 'amount' : bigint }, 76 | ) => Promise, 77 | } 78 | -------------------------------------------------------------------------------- /src/interfaces/icns_registry.ts: -------------------------------------------------------------------------------- 1 | import type { Principal } from '@dfinity/principal'; 2 | export interface ICNSRegistry { 3 | 'addWhitelist' : (arg_0: string) => Promise, 4 | 'approve' : (arg_0: string, arg_1: Principal) => Promise, 5 | 'balanceOf' : (arg_0: Principal) => Promise, 6 | 'controller' : (arg_0: string) => Promise<[] | [Principal]>, 7 | 'expiry' : (arg_0: string) => Promise<[] | [Time]>, 8 | 'exportOwnerDomains' : () => Promise]>>, 9 | 'getApproved' : (arg_0: string) => Promise<[] | [Principal]>, 10 | 'getControllerDomains' : (arg_0: Principal) => Promise< 11 | [] | [Array] 12 | >, 13 | 'getInfo' : () => Promise, 14 | 'getRecord' : (arg_0: string) => Promise<[] | [RecordExt]>, 15 | 'getUserDomains' : (arg_0: Principal) => Promise<[] | [Array]>, 16 | 'isApproved' : (arg_0: string, arg_1: Principal) => Promise, 17 | 'isApprovedForAll' : (arg_0: Principal, arg_1: Principal) => Promise, 18 | 'isWhitelisted' : (arg_0: string) => Promise, 19 | 'owner' : (arg_0: string) => Promise<[] | [Principal]>, 20 | 'recordExists' : (arg_0: string) => Promise, 21 | 'removeWhitelist' : (arg_0: string) => Promise, 22 | 'resolver' : (arg_0: string) => Promise<[] | [Principal]>, 23 | 'setApprovalForAll' : (arg_0: Principal, arg_1: boolean) => Promise< 24 | TxReceipt 25 | >, 26 | 'setController' : (arg_0: string, arg_1: Principal) => Promise, 27 | 'setOwner' : (arg_0: string, arg_1: Principal) => Promise, 28 | 'setRecord' : ( 29 | arg_0: string, 30 | arg_1: Principal, 31 | arg_2: Principal, 32 | arg_3: bigint, 33 | arg_4: Time, 34 | ) => Promise, 35 | 'setResolver' : (arg_0: string, arg_1: Principal) => Promise, 36 | 'setSubnodeExpiry' : (arg_0: string, arg_1: string, arg_2: Time) => Promise< 37 | TxReceipt 38 | >, 39 | 'setSubnodeOwner' : ( 40 | arg_0: string, 41 | arg_1: string, 42 | arg_2: Principal, 43 | ) => Promise, 44 | 'setSubnodeRecord' : ( 45 | arg_0: string, 46 | arg_1: string, 47 | arg_2: Principal, 48 | arg_3: Principal, 49 | arg_4: bigint, 50 | arg_5: Time, 51 | ) => Promise, 52 | 'setTTL' : (arg_0: string, arg_1: bigint) => Promise, 53 | 'transfer' : (arg_0: Principal, arg_1: string) => Promise, 54 | 'transferFrom' : ( 55 | arg_0: Principal, 56 | arg_1: Principal, 57 | arg_2: string, 58 | ) => Promise, 59 | 'ttl' : (arg_0: string) => Promise<[] | [bigint]>, 60 | } 61 | export interface Info { 62 | 'memSize' : bigint, 63 | 'heapSize' : bigint, 64 | 'historySize' : bigint, 65 | 'cycles' : bigint, 66 | 'names' : bigint, 67 | 'users' : bigint, 68 | } 69 | export interface RecordExt { 70 | 'ttl' : bigint, 71 | 'controller' : Principal, 72 | 'resolver' : Principal, 73 | 'owner' : Principal, 74 | 'operator' : Principal, 75 | 'name' : string, 76 | 'expiry' : Time, 77 | 'id': bigint, 78 | } 79 | export type Time = bigint; 80 | export type TxReceipt = { 'ok' : bigint } | 81 | { 'err' : string }; 82 | export default interface _SERVICE extends ICNSRegistry {}; 83 | -------------------------------------------------------------------------------- /src/interfaces/icns_resolver.ts: -------------------------------------------------------------------------------- 1 | import type { Principal } from '@dfinity/principal'; 2 | export interface DefaultInfoExt { 3 | 'btc' : [] | [string], 4 | 'eth' : [] | [string], 5 | 'icp' : [] | [string], 6 | 'pid' : [] | [Principal], 7 | 'url' : [] | [string], 8 | 'twitter' : [] | [string], 9 | 'host' : [] | [{ 'url' : string } | { 'canister' : Principal }], 10 | 'canisterExtensions' : Array<[string, Principal]>, 11 | 'description' : [] | [string], 12 | 'email' : [] | [string], 13 | 'textExtensions' : Array<[string, string]>, 14 | 'addrExtensions' : Array<[string, string]>, 15 | 'discord' : [] | [string], 16 | 'mainCanister' : [] | [Principal], 17 | 'telegram' : [] | [string], 18 | 'github' : [] | [string], 19 | 'avatar' : [] | [string], 20 | } 21 | export interface ICNSResolver { 22 | 'getAddr' : (arg_0: string, arg_1: string) => Promise<[] | [string]>, 23 | 'getCanister' : (arg_0: string, arg_1: string) => Promise<[] | [Principal]>, 24 | 'getExtensionLimit' : () => Promise, 25 | 'getHost' : (arg_0: string) => Promise< 26 | [] | [{ 'url' : string } | { 'canister' : Principal }] 27 | >, 28 | 'getInfo' : () => Promise, 29 | 'getLengthLimit' : () => Promise, 30 | 'getText' : (arg_0: string, arg_1: string) => Promise<[] | [string]>, 31 | 'getUserDefaultInfo' : (arg_0: string) => Promise<[] | [DefaultInfoExt]>, 32 | 'setAddr' : (arg_0: string, arg_1: string, arg_2: [] | [string]) => Promise< 33 | Result 34 | >, 35 | 'setCanister' : ( 36 | arg_0: string, 37 | arg_1: string, 38 | arg_2: [] | [Principal], 39 | ) => Promise, 40 | 'setExtensionLimit' : (arg_0: bigint) => Promise, 41 | 'setHost' : ( 42 | arg_0: string, 43 | arg_1: [] | [{ 'url' : string } | { 'canister' : Principal }], 44 | ) => Promise, 45 | 'setLengthLimit' : (arg_0: bigint) => Promise, 46 | 'setText' : (arg_0: string, arg_1: string, arg_2: [] | [string]) => Promise< 47 | Result 48 | >, 49 | } 50 | export interface Info { 51 | 'extensionLimit' : bigint, 52 | 'memSize' : bigint, 53 | 'heapSize' : bigint, 54 | 'maxRecordLength' : bigint, 55 | 'entries' : bigint, 56 | 'cycles' : bigint, 57 | } 58 | export type Result = { 'ok' : null } | 59 | { 'err' : string }; 60 | export default interface _SERVICE extends ICNSResolver {}; 61 | -------------------------------------------------------------------------------- /src/interfaces/icns_reverse_registrar.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { Principal } from '@dfinity/principal'; 3 | export interface ICNSReverseRegistrar { 4 | 'getName' : (arg_0: Principal) => Promise, 5 | 'setName' : (arg_0: string) => Promise, 6 | } 7 | export type Result = { 'ok' : string } | 8 | { 'err' : string }; 9 | export default interface _SERVICE extends ICNSReverseRegistrar {} -------------------------------------------------------------------------------- /src/interfaces/identity.ts: -------------------------------------------------------------------------------- 1 | declare type PublicKeyHex = string; 2 | declare type SecretKeyHex = string; 3 | export declare type JsonnableKeyPair = [PublicKeyHex, SecretKeyHex]; 4 | -------------------------------------------------------------------------------- /src/interfaces/ledger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/class-name-casing */ 2 | /* eslint-disable camelcase */ 3 | /* eslint-disable @typescript-eslint/camelcase */ 4 | import { Principal } from '@dfinity/principal'; 5 | 6 | export interface AccountBalanceArgs { 7 | account: AccountIdentifier; 8 | } 9 | export type AccountIdentifier = string; 10 | export interface ArchiveOptions { 11 | max_message_size_bytes: [] | [number]; 12 | node_max_memory_size_bytes: [] | [number]; 13 | controller_id: Principal; 14 | } 15 | export type BlockHeight = bigint; 16 | export interface Duration { 17 | secs: bigint; 18 | nanos: number; 19 | } 20 | export interface ICPTs { 21 | e8s: bigint; 22 | } 23 | export interface LedgerCanisterInitPayload { 24 | send_whitelist: Array<[Principal]>; 25 | minting_account: AccountIdentifier; 26 | transaction_window: [] | [Duration]; 27 | max_message_size_bytes: [] | [number]; 28 | archive_options: [] | [ArchiveOptions]; 29 | initial_values: Array<[AccountIdentifier, ICPTs]>; 30 | } 31 | export type Memo = bigint; 32 | export interface NotifyCanisterArgs { 33 | to_subaccount: [] | [SubAccount]; 34 | from_subaccount: [] | [SubAccount]; 35 | to_canister: Principal; 36 | max_fee: ICPTs; 37 | block_height: BlockHeight; 38 | } 39 | export interface SendArgs { 40 | to: AccountIdentifier; 41 | fee: ICPTs; 42 | memo: Memo; 43 | from_subaccount: [] | [SubAccount]; 44 | created_at_time: [] | [TimeStamp]; 45 | amount: ICPTs; 46 | } 47 | export type SubAccount = Array; 48 | export interface TimeStamp { 49 | timestamp_nanos: bigint; 50 | } 51 | export interface Transaction { 52 | memo: Memo; 53 | created_at: BlockHeight; 54 | transfer: Transfer; 55 | } 56 | export type Transfer = 57 | | { 58 | Burn: { from: AccountIdentifier; amount: ICPTs }; 59 | } 60 | | { Mint: { to: AccountIdentifier; amount: ICPTs } } 61 | | { 62 | Send: { 63 | to: AccountIdentifier; 64 | from: AccountIdentifier; 65 | amount: ICPTs; 66 | }; 67 | }; 68 | export default interface _SERVICE { 69 | account_balance_dfx: (arg_0: AccountBalanceArgs) => Promise; 70 | notify_dfx: (arg_0: NotifyCanisterArgs) => Promise; 71 | send_dfx: (arg_0: SendArgs) => Promise; 72 | } 73 | -------------------------------------------------------------------------------- /src/interfaces/nns_uid.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/class-name-casing */ 2 | /* eslint-disable camelcase */ 3 | /* eslint-disable @typescript-eslint/camelcase */ 4 | import { Principal } from '@dfinity/principal'; 5 | 6 | export interface AccountDetails { 7 | account_identifier: AccountIdentifier; 8 | hardware_wallet_accounts: Array; 9 | sub_accounts: Array; 10 | } 11 | export type AccountIdentifier = string; 12 | export interface AttachCanisterRequest { 13 | name: string; 14 | canister_id: Principal; 15 | } 16 | export type AttachCanisterResponse = 17 | | { Ok: null } 18 | | { CanisterAlreadyAttached: null } 19 | | { NameAlreadyTaken: null } 20 | | { NameTooLong: null } 21 | | { CanisterLimitExceeded: null }; 22 | export type BlockHeight = bigint; 23 | export interface CanisterDetails { 24 | name: string; 25 | canister_id: Principal; 26 | } 27 | export type CreateSubAccountResponse = 28 | | { Ok: SubAccountDetails } 29 | | { AccountNotFound: null } 30 | | { NameTooLong: null } 31 | | { SubAccountLimitExceeded: null }; 32 | export interface DetachCanisterRequest { 33 | canister_id: Principal; 34 | } 35 | export type DetachCanisterResponse = { Ok: null } | { CanisterNotFound: null }; 36 | export type GetAccountResponse = 37 | | { Ok: AccountDetails } 38 | | { AccountNotFound: null }; 39 | export interface GetTransactionsRequest { 40 | page_size: number; 41 | offset: number; 42 | account_identifier: AccountIdentifier; 43 | } 44 | export interface GetTransactionsResponse { 45 | total: number; 46 | transactions: Array; 47 | } 48 | export interface HardwareWalletAccountDetails { 49 | name: string; 50 | account_identifier: AccountIdentifier; 51 | } 52 | export type HeaderField = [string, string]; 53 | export interface HttpRequest { 54 | url: string; 55 | method: string; 56 | body: Array; 57 | headers: Array; 58 | } 59 | export interface HttpResponse { 60 | body: Array; 61 | headers: Array; 62 | status_code: number; 63 | } 64 | export interface ICPTs { 65 | e8s: bigint; 66 | } 67 | export interface Receive { 68 | fee: ICPTs; 69 | from: AccountIdentifier; 70 | amount: ICPTs; 71 | } 72 | export interface RegisterHardwareWalletRequest { 73 | name: string; 74 | account_identifier: AccountIdentifier; 75 | } 76 | export type RegisterHardwareWalletResponse = 77 | | { Ok: null } 78 | | { AccountNotFound: null } 79 | | { HardwareWalletAlreadyRegistered: null } 80 | | { HardwareWalletLimitExceeded: null } 81 | | { NameTooLong: null }; 82 | export interface RemoveHardwareWalletRequest { 83 | account_identifier: AccountIdentifier; 84 | } 85 | export type RemoveHardwareWalletResponse = 86 | | { Ok: null } 87 | | { HardwareWalletNotFound: null }; 88 | export interface RenameSubAccountRequest { 89 | new_name: string; 90 | account_identifier: AccountIdentifier; 91 | } 92 | export type RenameSubAccountResponse = 93 | | { Ok: null } 94 | | { AccountNotFound: null } 95 | | { SubAccountNotFound: null } 96 | | { NameTooLong: null }; 97 | export interface Send { 98 | to: AccountIdentifier; 99 | fee: ICPTs; 100 | amount: ICPTs; 101 | } 102 | export interface Stats { 103 | latest_transaction_block_height: BlockHeight; 104 | seconds_since_last_ledger_sync: bigint; 105 | sub_accounts_count: bigint; 106 | hardware_wallet_accounts_count: bigint; 107 | accounts_count: bigint; 108 | earliest_transaction_block_height: BlockHeight; 109 | transactions_count: bigint; 110 | block_height_synced_up_to: [] | [bigint]; 111 | latest_transaction_timestamp_nanos: bigint; 112 | earliest_transaction_timestamp_nanos: bigint; 113 | } 114 | export type SubAccount = Array; 115 | export interface SubAccountDetails { 116 | name: string; 117 | sub_account: SubAccount; 118 | account_identifier: AccountIdentifier; 119 | } 120 | export interface Timestamp { 121 | timestamp_nanos: bigint; 122 | } 123 | export interface Transaction { 124 | memo: bigint; 125 | timestamp: Timestamp; 126 | block_height: BlockHeight; 127 | transfer: Transfer; 128 | } 129 | export type Transfer = 130 | | { Burn: { amount: ICPTs } } 131 | | { Mint: { amount: ICPTs } } 132 | | { Send: Send } 133 | | { Receive: Receive }; 134 | export default interface _SERVICE { 135 | add_account: () => Promise; 136 | attach_canister: ( 137 | arg_0: AttachCanisterRequest 138 | ) => Promise; 139 | create_sub_account: (arg_0: string) => Promise; 140 | detach_canister: ( 141 | arg_0: DetachCanisterRequest 142 | ) => Promise; 143 | get_account: () => Promise; 144 | get_canisters: () => Promise>; 145 | get_icp_to_cycles_conversion_rate: () => Promise; 146 | get_stats: () => Promise; 147 | get_transactions: ( 148 | arg_0: GetTransactionsRequest 149 | ) => Promise; 150 | http_request: (arg_0: HttpRequest) => Promise; 151 | register_hardware_wallet: ( 152 | arg_0: RegisterHardwareWalletRequest 153 | ) => Promise; 154 | remove_hardware_wallet: ( 155 | arg_0: RemoveHardwareWalletRequest 156 | ) => Promise; 157 | rename_sub_account: ( 158 | arg_0: RenameSubAccountRequest 159 | ) => Promise; 160 | } 161 | -------------------------------------------------------------------------------- /src/interfaces/plug_keyring.ts: -------------------------------------------------------------------------------- 1 | import type PlugWallet from '../PlugWallet' 2 | import { 3 | JSONWallet 4 | } from '../interfaces/plug_wallet'; 5 | 6 | export interface PlugState { 7 | password?: string; 8 | currentWalletId?: string; 9 | mnemonicWalletCount: number; 10 | walletIds?: Array 11 | } 12 | export interface PlugStateStorage extends PlugState { 13 | wallets: { [key : string]: JSONWallet }; 14 | } 15 | 16 | export interface PlugStateInstance extends PlugState { 17 | wallets: { [key : string]: PlugWallet }; 18 | } 19 | -------------------------------------------------------------------------------- /src/interfaces/plug_wallet.ts: -------------------------------------------------------------------------------- 1 | import { NFTCollection } from '@psychedelic/dab-js'; 2 | import { Network, NetworkModuleParams } from '../PlugKeyRing/modules/NetworkModule'; 3 | import { ConnectedApp } from './account'; 4 | import { TokenBalance } from './token'; 5 | import { Types } from '../utils/account/constants'; 6 | import { GenericSignIdentity } from '../utils/identity/genericSignIdentity' 7 | 8 | 9 | export interface ICNSData { 10 | names?: string[]; 11 | reverseResolvedName?: string; 12 | } 13 | 14 | export interface PlugWalletArgs { 15 | name?: string; 16 | walletId: string; 17 | orderNumber: number; 18 | walletNumber?: number; 19 | icon?: string; 20 | fetch: any; 21 | icnsData?: { names?: string[]; reverseResolvedName?: string }; 22 | network: Network, 23 | identity: GenericSignIdentity, 24 | type: Types, 25 | } 26 | 27 | export interface Assets { 28 | [canisterId: string]: TokenBalance 29 | } 30 | 31 | export interface JSONWallet { 32 | name: string; 33 | walletId: string; 34 | orderNumber: number; 35 | walletNumber?: number; 36 | principal: string; 37 | accountId: string; 38 | icon?: string; 39 | icnsData: ICNSData; 40 | networkModule?: NetworkModuleParams; 41 | type: Types; 42 | keyPair: string; 43 | } 44 | 45 | export interface NFTDetailsBase { 46 | index: idT; 47 | canister: string; 48 | url: string; 49 | standard: string; 50 | } 51 | 52 | export interface WalletNFTCollection extends Omit { 53 | tokens: NFTDetailsBase[]; 54 | } 55 | 56 | export interface WalletNFTInfo extends NFTCollection { 57 | registeredBy: Array; 58 | } 59 | -------------------------------------------------------------------------------- /src/interfaces/storage.ts: -------------------------------------------------------------------------------- 1 | import { NetworkModuleParams } from '../PlugKeyRing/modules/NetworkModule'; 2 | import { PlugStateStorage } from './plug_keyring' 3 | 4 | export interface StorageData { 5 | vault: PlugStateStorage; 6 | isInitialized: boolean; 7 | isUnlocked: boolean; 8 | currentWalletId: string; 9 | version: string; 10 | networkModule: NetworkModuleParams; 11 | } 12 | 13 | export interface KeyringStorage { 14 | isSupported: boolean; 15 | get: (key?: string) => Promise; 16 | set: (state: unknown) => Promise; 17 | clear: () => Promise; 18 | } 19 | -------------------------------------------------------------------------------- /src/interfaces/token.ts: -------------------------------------------------------------------------------- 1 | export interface StandardToken { 2 | name: string; 3 | symbol: string; 4 | canisterId: string; 5 | standard: string; 6 | decimals: number; 7 | color?: string; 8 | logo?: string; 9 | fee?: bigint | number; 10 | } 11 | 12 | export interface TokenBalance { 13 | amount: string; 14 | token: StandardToken; 15 | error?: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/interfaces/transactions.ts: -------------------------------------------------------------------------------- 1 | export interface InferredTransaction { 2 | hash: string; 3 | timestamp: bigint; 4 | type: string; 5 | details?: { [key: string]: any }; 6 | caller: string; 7 | } 8 | 9 | export type Nullable = T | null; 10 | 11 | export interface FormattedTransaction { 12 | type: string; 13 | to: string; 14 | from: string; 15 | hash: string; 16 | amount: Nullable; // borrar esto 17 | value?: Nullable; // borrar esto 18 | decimal?: number; 19 | status: number; 20 | date: bigint; 21 | symbol: string; 22 | logo: string; 23 | canisterId: string; 24 | details?: { [key: string]: any }; 25 | canisterInfo?: Object; 26 | } 27 | 28 | export interface GetTransactionsResponse { 29 | total: number; 30 | transactions: InferredTransaction[]; 31 | } 32 | 33 | export interface FormattedTransactions { 34 | total: number; 35 | transactions: FormattedTransaction[]; 36 | } 37 | -------------------------------------------------------------------------------- /src/interfaces/wallet.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | /* eslint-disable camelcase */ 3 | /* eslint-disable @typescript-eslint/camelcase */ 4 | import type { Principal } from '@dfinity/principal'; 5 | 6 | export interface AddressEntry { 7 | id: Principal; 8 | kind: Kind; 9 | name: [] | [string]; 10 | role: Role; 11 | } 12 | export interface CanisterSettings { 13 | controller: [] | [Principal]; 14 | freezing_threshold: [] | [bigint]; 15 | memory_allocation: [] | [bigint]; 16 | compute_allocation: [] | [bigint]; 17 | } 18 | export interface CreateCanisterArgs { 19 | cycles: bigint; 20 | settings: CanisterSettings; 21 | } 22 | export interface Event { 23 | id: number; 24 | kind: EventKind; 25 | timestamp: bigint; 26 | } 27 | export type EventKind = 28 | | { 29 | CyclesReceived: { from: Principal; amount: bigint }; 30 | } 31 | | { CanisterCreated: { cycles: bigint; canister: Principal } } 32 | | { 33 | CanisterCalled: { 34 | cycles: bigint; 35 | method_name: string; 36 | canister: Principal; 37 | }; 38 | } 39 | | { 40 | CyclesSent: { to: Principal; amount: bigint; refund: bigint }; 41 | } 42 | | { AddressRemoved: { id: Principal } } 43 | | { WalletDeployed: { canister: Principal } } 44 | | { 45 | AddressAdded: { id: Principal; name: [] | [string]; role: Role }; 46 | }; 47 | export type Kind = { User: null } | { Canister: null } | { Unknown: null }; 48 | export type ResultCall = { Ok: { return: Array } } | { Err: string }; 49 | export type ResultCreate = { Ok: { canister_id: Principal } } | { Err: string }; 50 | export type ResultSend = { Ok: null } | { Err: string }; 51 | export type Role = 52 | | { Custodian: null } 53 | | { Contact: null } 54 | | { Controller: null }; 55 | 56 | export default interface WalletService { 57 | add_address: (arg_0: AddressEntry) => Promise; 58 | add_controller: (arg_0: Principal) => Promise; 59 | authorize: (arg_0: Principal) => Promise; 60 | deauthorize: (arg_0: Principal) => Promise; 61 | get_chart: ( 62 | arg_0: [] | [{ count: [] | [number]; precision: [] | [bigint] }] 63 | ) => Promise>; 64 | get_controllers: () => Promise>; 65 | get_custodians: () => Promise>; 66 | get_events: ( 67 | arg_0: [] | [{ to: [] | [number]; from: [] | [number] }] 68 | ) => Promise>; 69 | list_addresses: () => Promise>; 70 | name: () => Promise<[] | [string]>; 71 | remove_address: (arg_0: Principal) => Promise; 72 | remove_controller: (arg_0: Principal) => Promise; 73 | set_name: (arg_0: string) => Promise; 74 | wallet_balance: () => Promise<{ amount: bigint }>; 75 | wallet_call: (arg_0: { 76 | args: Array; 77 | cycles: bigint; 78 | method_name: string; 79 | canister: Principal; 80 | }) => Promise; 81 | wallet_create_canister: (arg_0: CreateCanisterArgs) => Promise; 82 | wallet_create_wallet: (arg_0: CreateCanisterArgs) => Promise; 83 | wallet_receive: () => Promise; 84 | wallet_send: (arg_0: { 85 | canister: Principal; 86 | amount: bigint; 87 | }) => Promise; 88 | wallet_store_wallet_wasm: (arg_0: { 89 | wasm_module: Array; 90 | }) => Promise; 91 | } -------------------------------------------------------------------------------- /src/jest/setup-jest.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | -------------------------------------------------------------------------------- /src/utils/account/account.test.ts: -------------------------------------------------------------------------------- 1 | import { Principal } from '@dfinity/principal'; 2 | import { ERRORS } from '../../errors'; 3 | import { AccountCredentials } from '../../interfaces/account'; 4 | import { Secp256k1KeyPair } from '../crypto/keys'; 5 | import { verify, sign } from '../signature'; 6 | import { 7 | createAccount, 8 | createAccountFromMnemonic, 9 | getAccountId, 10 | } from './index'; 11 | 12 | describe('Account utils', () => { 13 | let globalAccount: AccountCredentials; 14 | let globalKeys: Secp256k1KeyPair; 15 | const MAX_ACCOUNTS = 5; 16 | 17 | beforeAll(() => { 18 | globalAccount = createAccount(); 19 | globalKeys = globalAccount.identity.getKeyPair(); 20 | }); 21 | 22 | describe('credentials creation', () => { 23 | it('should create a new account with mnemonic, secret and public keys', () => { 24 | const account = createAccount(); 25 | expect(account).toHaveProperty('mnemonic'); 26 | expect(account).toHaveProperty('identity'); 27 | expect(account).toHaveProperty('accountId'); 28 | expect(account.identity.getKeyPair()).toHaveProperty('secretKey'); 29 | expect(account.identity.getKeyPair()).toHaveProperty('publicKey'); 30 | }); 31 | 32 | it('should always create different new accounts', () => { 33 | const mnemonics: string[] = []; 34 | const secretKeys: string[] = []; 35 | const publicKeys: string[] = []; 36 | for (let i = 1; i < MAX_ACCOUNTS; i += 1) { 37 | const { mnemonic, identity } = createAccount(); 38 | const { publicKey, secretKey } = identity.getKeyPair(); 39 | expect(mnemonics).not.toContain(mnemonic); 40 | expect(secretKeys).not.toContain(secretKey); 41 | expect(publicKeys).not.toContain(publicKey); 42 | 43 | mnemonics.push(mnemonic); 44 | secretKeys.push(secretKey.toString()); 45 | publicKeys.push(publicKey.toString()); 46 | } 47 | }); 48 | 49 | it('should create a new account from a mnemonic, having new secret and public keys', () => { 50 | const account = createAccountFromMnemonic(globalAccount.mnemonic, 1); 51 | 52 | expect(account).toHaveProperty('mnemonic'); 53 | expect(account).toHaveProperty('identity'); 54 | expect(account).toHaveProperty('accountId'); 55 | 56 | expect(account.identity.getKeyPair()).toHaveProperty('secretKey'); 57 | expect(account.identity.getKeyPair()).toHaveProperty('publicKey'); 58 | 59 | const { mnemonic, identity, accountId } = account; 60 | const { publicKey, secretKey } = identity.getKeyPair(); 61 | 62 | // Mnemonic should be the same but keys and accountId shouldn't 63 | expect(mnemonic).toEqual(globalAccount.mnemonic); 64 | expect(accountId).not.toEqual(globalAccount.accountId); 65 | expect(secretKey).not.toEqual(globalKeys.secretKey.toString()); 66 | expect(publicKey).not.toEqual(globalKeys.publicKey.toDer().toString()); 67 | }); 68 | 69 | it('should always derive the same account given the same mnemonic and account ID', () => { 70 | for (let i = 0; i < MAX_ACCOUNTS; i += 1) { 71 | const account = createAccountFromMnemonic(globalAccount.mnemonic, i); 72 | const newAccount = createAccountFromMnemonic(globalAccount.mnemonic, i); 73 | const { secretKey, publicKey } = account.identity.getKeyPair(); 74 | const { 75 | secretKey: newSecret, 76 | publicKey: newPublic, 77 | } = newAccount.identity.getKeyPair(); 78 | 79 | expect(account.mnemonic).toEqual(newAccount.mnemonic); 80 | expect(account.accountId).toEqual(newAccount.accountId); 81 | expect(secretKey).toEqual(newSecret); 82 | expect(publicKey).toEqual(newPublic); 83 | } 84 | }); 85 | 86 | it('should generate the correct principal id', () => { 87 | const mnemonic = 88 | 'easily drift crazy brother trash green cricket peasant unhappy fruit behind pudding'; 89 | const principal = 90 | 'gkuhp-3onv2-yuitx-msib3-z4kyb-uw5ua-fehux-6ontl-47u47-iwuul-rae'; 91 | const { identity } = createAccountFromMnemonic(mnemonic, 0); 92 | expect(identity.getPrincipal().toText()).toEqual(principal); 93 | }); 94 | 95 | it('should fail if provided an invalid mnemonic', () => { 96 | const invalidMnemonic = 'Some invalid Mnemonic'; 97 | expect(() => createAccountFromMnemonic(invalidMnemonic, 1)).toThrow( 98 | ERRORS.INVALID_MNEMONIC 99 | ); 100 | }); 101 | 102 | it('should fail if provided an invalid account id', () => { 103 | const stringId = '1'; 104 | const negativeId = -1; 105 | expect(() => 106 | createAccountFromMnemonic(globalAccount.mnemonic, stringId as any) 107 | ).toThrow(ERRORS.INVALID_ACC_ID); 108 | expect(() => 109 | createAccountFromMnemonic(globalAccount.mnemonic, negativeId) 110 | ).toThrow(ERRORS.INVALID_ACC_ID); 111 | }); 112 | 113 | // This checks that this works on .js files as well as TS which auto-checks these things 114 | it('should fail if provided an empty mnemonic', () => { 115 | expect(() => createAccountFromMnemonic('', 1)).toThrow( 116 | ERRORS.INVALID_MNEMONIC 117 | ); 118 | expect(() => createAccountFromMnemonic(undefined as any, 1)).toThrow( 119 | ERRORS.INVALID_MNEMONIC 120 | ); 121 | expect(() => createAccountFromMnemonic(null as any, 1)).toThrow( 122 | ERRORS.INVALID_MNEMONIC 123 | ); 124 | }); 125 | }); 126 | 127 | describe('id generation', () => { 128 | it('should generate the correct account id', () => { 129 | const principal = Principal.fromText( 130 | 'gkuhp-3onv2-yuitx-msib3-z4kyb-uw5ua-fehux-6ontl-47u47-iwuul-rae' 131 | ); 132 | const accountId = 133 | '1f77688a6a9b2b85640d753d487209344cc9c9675c409bbef5e061710c7220ab'; 134 | const id = getAccountId(principal); 135 | expect(id).toEqual(accountId); 136 | }); 137 | }); 138 | 139 | describe('credentials utility', () => { 140 | test('should sign a message into an unreadable state and recover it using its keys', () => { 141 | const { secretKey, publicKey } = globalKeys; 142 | const message = 'This is a secret message!'; 143 | const signed = sign(message, secretKey); 144 | const opened = verify(message, signed, publicKey); 145 | expect(opened).toBe(true); 146 | }); 147 | }); 148 | }); 149 | -------------------------------------------------------------------------------- /src/utils/account/constants.ts: -------------------------------------------------------------------------------- 1 | // ED25519 key derivation path 2 | export const DERIVATION_PATH = "m/44'/223'/0'/0"; 3 | 4 | // Dfinity Account separator 5 | export const ACCOUNT_DOMAIN_SEPERATOR = '\x0Aaccount-id'; 6 | 7 | // Subaccounts are arbitrary 32-byte values. 8 | export const SUB_ACCOUNT_ZERO = Buffer.alloc(32); 9 | 10 | export const HARDENED_OFFSET = 0x80000000; 11 | 12 | export enum Types { 13 | mnemonic = "MNEMONIC", 14 | pem256k1 = "PEM_256k1", 15 | pem25519 = "PEM_25519", 16 | secretKey256k1 = "SECRET_KEY_256k1", 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/account/index.ts: -------------------------------------------------------------------------------- 1 | import * as bip39 from 'bip39'; 2 | import CryptoJS from 'crypto-js'; 3 | import { Principal } from '@dfinity/principal'; 4 | 5 | import { ERRORS } from '../../errors'; 6 | import Secp256k1KeyIdentity from '../identity/secpk256k1/identity'; 7 | import { AccountCredentials } from '../../interfaces/account'; 8 | import { ACCOUNT_DOMAIN_SEPERATOR, SUB_ACCOUNT_ZERO } from './constants'; 9 | import { 10 | byteArrayToWordArray, 11 | generateChecksum, 12 | wordArrayToByteArray, 13 | } from '../crypto/binary'; 14 | import { createSecp256K1KeyPair } from '../crypto/keys'; 15 | 16 | interface DerivedKey { 17 | key: Buffer; 18 | chainCode: Buffer; 19 | } 20 | 21 | /* 22 | Used dfinity/keysmith/account/account.go as a base for the ID generation 23 | */ 24 | export const getAccountId = ( 25 | principal: Principal, 26 | subaccount?: number, 27 | cryptoAdapter = CryptoJS 28 | ): string => { 29 | const sha = cryptoAdapter.algo.SHA224.create(); 30 | sha.update(ACCOUNT_DOMAIN_SEPERATOR); // Internally parsed with UTF-8, like go does 31 | sha.update(byteArrayToWordArray(principal.toUint8Array())); 32 | const subBuffer = Buffer.from(SUB_ACCOUNT_ZERO); 33 | if (subaccount) { 34 | subBuffer.writeUInt32BE(subaccount); 35 | } 36 | sha.update(byteArrayToWordArray(subBuffer)); 37 | const hash = sha.finalize(); 38 | 39 | /// While this is backed by an array of length 28, it's canonical representation 40 | /// is a hex string of length 64. The first 8 characters are the CRC-32 encoded 41 | /// hash of the following 56 characters of hex. Both, upper and lower case 42 | /// characters are valid in the input string and can even be mixed. 43 | /// [ic/rs/rosetta-api/ledger_canister/src/account_identifier.rs] 44 | const byteArray = wordArrayToByteArray(hash, 28); 45 | const checksum = generateChecksum(byteArray); 46 | const val = checksum + hash.toString(); 47 | 48 | return val; 49 | }; 50 | 51 | const getAccountCredentials = ( 52 | mnemonic: string, 53 | subaccount?: number 54 | ): AccountCredentials => { 55 | const keyPair = createSecp256K1KeyPair(mnemonic, subaccount || 0); 56 | // Identity has boths keys via getKeyPair and PID via getPrincipal 57 | const identity = Secp256k1KeyIdentity.fromKeyPair( 58 | keyPair.publicKey.toRaw(), 59 | keyPair.secretKey 60 | ); 61 | const accountId = getAccountId(identity.getPrincipal()); 62 | return { 63 | mnemonic, 64 | identity, 65 | accountId, 66 | }; 67 | }; 68 | 69 | export const createAccount = (entropy?: Buffer): AccountCredentials => { 70 | const mnemonic = entropy 71 | ? bip39.entropyToMnemonic(entropy) 72 | : bip39.generateMnemonic(); 73 | return getAccountCredentials(mnemonic, 0); 74 | }; 75 | 76 | export const createAccountFromMnemonic = ( 77 | mnemonic: string, 78 | accountId: number 79 | ): AccountCredentials => { 80 | if (!mnemonic || !bip39.validateMnemonic(mnemonic)) { 81 | throw new Error(ERRORS.INVALID_MNEMONIC); 82 | } 83 | if (typeof accountId !== 'number' || accountId < 0) { 84 | throw new Error(ERRORS.INVALID_ACC_ID); 85 | } 86 | return getAccountCredentials(mnemonic, accountId); 87 | }; 88 | -------------------------------------------------------------------------------- /src/utils/array.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | export const uniqueByObjKey = (arr: Array, key: string): Array => [ 3 | ...new Map(arr.map(item => [item[key], item])).values(), 4 | ]; 5 | 6 | export function uniqueMap(array: Array, mapFunction: (item: T) => K | undefined): Array { 7 | return [...new Set(array.map(mapFunction))].filter(value => value) as Array; 8 | }; 9 | -------------------------------------------------------------------------------- /src/utils/crypto/binary.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-prototype-builtins */ 2 | /* eslint-disable no-bitwise */ 3 | import crc32 from 'buffer-crc32'; 4 | import CryptoJS from 'crypto-js'; 5 | 6 | export const byteArrayToWordArray = ( 7 | byteArray: Uint8Array, 8 | cryptoAdapter = CryptoJS 9 | ) => { 10 | const wordArray = [] as any; 11 | let i; 12 | for (i = 0; i < byteArray.length; i += 1) { 13 | wordArray[(i / 4) | 0] |= byteArray[i] << (24 - 8 * i); 14 | } 15 | // eslint-disable-next-line 16 | const result = cryptoAdapter.lib.WordArray.create( 17 | wordArray, 18 | byteArray.length 19 | ); 20 | return result; 21 | }; 22 | 23 | export const wordToByteArray = (word, length): number[] => { 24 | const byteArray: number[] = []; 25 | const xFF = 0xff; 26 | if (length > 0) byteArray.push(word >>> 24); 27 | if (length > 1) byteArray.push((word >>> 16) & xFF); 28 | if (length > 2) byteArray.push((word >>> 8) & xFF); 29 | if (length > 3) byteArray.push(word & xFF); 30 | 31 | return byteArray; 32 | }; 33 | 34 | export const wordArrayToByteArray = (wordArray, length) => { 35 | if ( 36 | wordArray.hasOwnProperty('sigBytes') && 37 | wordArray.hasOwnProperty('words') 38 | ) { 39 | length = wordArray.sigBytes; 40 | wordArray = wordArray.words; 41 | } 42 | 43 | let result: number[] = []; 44 | let bytes; 45 | let i = 0; 46 | while (length > 0) { 47 | bytes = wordToByteArray(wordArray[i], Math.min(4, length)); 48 | length -= bytes.length; 49 | result = [...result, bytes]; 50 | i++; 51 | } 52 | return [].concat.apply([], result); 53 | }; 54 | 55 | export const intToHex = (val: number) => 56 | val < 0 ? (Number(val) >>> 0).toString(16) : Number(val).toString(16); 57 | 58 | // We generate a CRC32 checksum, and trnasform it into a hexString 59 | export const generateChecksum = (hash: Uint8Array) => { 60 | const crc = crc32.unsigned(Buffer.from(hash)); 61 | const hex = intToHex(crc); 62 | return hex.padStart(8, '0'); 63 | }; 64 | 65 | export const lebDecode = pipe => { 66 | let weight = BigInt(1); 67 | let value = BigInt(0); 68 | let byte; 69 | do { 70 | if (pipe.length < 1) { 71 | throw new Error('unexpected end of buffer'); 72 | } 73 | byte = pipe[0]; 74 | pipe = pipe.slice(1); 75 | value += BigInt(byte & 0x7f).valueOf() * weight; 76 | weight *= BigInt(128); 77 | } while (byte >= 0x80); 78 | return value; 79 | }; 80 | -------------------------------------------------------------------------------- /src/utils/crypto/keys.ts: -------------------------------------------------------------------------------- 1 | import tweetnacl from 'tweetnacl'; 2 | import * as bip39 from 'bip39'; 3 | import { derivePath } from 'ed25519-hd-key'; 4 | import HDKey from 'hdkey'; 5 | import Secp256k1 from 'secp256k1'; 6 | import { BinaryBlob, blobFromUint8Array } from '@dfinity/candid'; 7 | 8 | import { DERIVATION_PATH } from '../account/constants'; 9 | import Secp256k1PublicKey from '../identity/secpk256k1/publicKey'; 10 | 11 | export interface Secp256k1KeyPair { 12 | publicKey: Secp256k1PublicKey; 13 | secretKey: BinaryBlob; 14 | } 15 | 16 | export const createKeyPair = ( 17 | mnemonic: string, 18 | index = 0 19 | ): tweetnacl.SignKeyPair => { 20 | // Generate bip39 mnemonic. [Curve agnostic] 21 | const seed = bip39.mnemonicToSeedSync(mnemonic); 22 | 23 | // Derive key using dfinity's path 24 | const { key } = derivePath(DERIVATION_PATH, seed.toString('hex'), index); 25 | return tweetnacl.sign.keyPair.fromSeed(key); 26 | }; 27 | 28 | export const createSecp256K1KeyPair = ( 29 | mnemonic: string, 30 | index = 0 31 | ): Secp256k1KeyPair => { 32 | // Generate bip39 mnemonic. [Curve agnostic] 33 | const seed = bip39.mnemonicToSeedSync(mnemonic); 34 | const masterKey = HDKey.fromMasterSeed(seed); 35 | 36 | // BIP 44 derivation path definition 37 | // m / purpose' / coin_type' / account' / change / address_index ---> this being the subaccount index 38 | const { privateKey } = masterKey.derive(`${DERIVATION_PATH}/${index}`); 39 | const publicKey = Secp256k1.publicKeyCreate(privateKey, false); 40 | return { 41 | secretKey: privateKey, 42 | publicKey: Secp256k1PublicKey.fromRaw(blobFromUint8Array(publicKey)), 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /src/utils/dab.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getCanisterInfo as getCanisterInfoFromDab, 3 | getMultipleCanisterInfo as getMultipleCanisterInfoFromDab, 4 | } from '@psychedelic/dab-js'; 5 | import { HttpAgent } from '@dfinity/agent'; 6 | import { Principal } from '@dfinity/principal'; 7 | import crossFetch from 'cross-fetch'; 8 | 9 | import { PLUG_PROXY_HOST } from './dfx/constants'; 10 | import { wrappedFetch } from './dfx/wrappedFetch'; 11 | 12 | export interface CanisterInfo { 13 | canisterId: string; 14 | name: string; 15 | description: string; 16 | icon: string; 17 | } 18 | 19 | export const getCanisterInfo = async ( 20 | canisterId: string, 21 | agent?: HttpAgent, 22 | fetch?: typeof crossFetch 23 | ): Promise => { 24 | const finalAgent = 25 | agent || 26 | new HttpAgent({ 27 | host: PLUG_PROXY_HOST, 28 | fetch: fetch ? wrappedFetch(fetch) : wrappedFetch(), 29 | }); 30 | 31 | const result = await getCanisterInfoFromDab({ 32 | canisterId, 33 | agent: finalAgent, 34 | }); 35 | if (result) return { ...result, icon: result.logo_url }; 36 | return undefined; 37 | }; 38 | 39 | export const getMultipleCanisterInfo = async ( 40 | canisterIds: string[], 41 | agent?: HttpAgent, 42 | fetch?: typeof crossFetch 43 | ): Promise => { 44 | const finalAgent = 45 | agent || 46 | new HttpAgent({ 47 | host: PLUG_PROXY_HOST, 48 | fetch: fetch ? wrappedFetch(fetch) : wrappedFetch(), 49 | }); 50 | 51 | const result = await getMultipleCanisterInfoFromDab({ 52 | canisterIds: canisterIds.map(id => Principal.from(id)), 53 | agent: finalAgent, 54 | }); 55 | 56 | if (!result) return []; 57 | 58 | return result.map(canisterMetadata => ({ 59 | ...canisterMetadata, 60 | icon: canisterMetadata.logo_url, 61 | })); 62 | }; 63 | -------------------------------------------------------------------------------- /src/utils/dfx/actorFactory.ts: -------------------------------------------------------------------------------- 1 | import { HttpAgent, Actor, ActorMethod, ActorSubclass } from '@dfinity/agent'; 2 | import { IDL } from '@dfinity/candid'; 3 | import { Principal } from '@dfinity/principal'; 4 | 5 | type ExtendedActorConstructor = new () => ActorSubclass; 6 | 7 | export type BaseMethodsExtendedActor = { 8 | [K in keyof T as `_${Uncapitalize}`]: T[K]; 9 | } 10 | 11 | export const createExtendedActorClass = ( 12 | agent: HttpAgent, 13 | methods, 14 | canisterId: string | Principal, 15 | IDLFactory: IDL.InterfaceFactory 16 | ): ExtendedActorConstructor => { 17 | class ExtendedActor extends Actor.createActorClass(IDLFactory) { 18 | constructor() { 19 | super({ agent, canisterId }); 20 | 21 | Object.keys(this).forEach(methodName => { 22 | this[`_${methodName}`] = this[methodName]; 23 | }) 24 | 25 | Object.keys(methods).forEach(methodName => { 26 | this[methodName] = ((...args: unknown[]) => 27 | methods[methodName](this, ...args) as unknown) as ActorMethod; 28 | }); 29 | } 30 | } 31 | 32 | return ExtendedActor; 33 | }; 34 | 35 | export default { createExtendedActorClass }; 36 | -------------------------------------------------------------------------------- /src/utils/dfx/constants.ts: -------------------------------------------------------------------------------- 1 | export const LEDGER_CANISTER_ID = 'ryjl3-tyaaa-aaaaa-aaaba-cai'; 2 | export const NNS_CANISTER_ID = 'qoctq-giaaa-aaaaa-aaaea-cai'; 3 | export const PLUG_PROXY_HOST = 'https://mainnet.plugwallet.ooo/'; 4 | export const IC_URL_HOST = 'ic0.app'; 5 | export const NET_ID = { 6 | blockchain: 'Internet Computer', 7 | network: '00000000000000020101', 8 | }; 9 | export const ROSETTA_URL = 'https://rosetta-api.internetcomputer.org'; 10 | export const PRINCIPAL_REGEX = /(\w{5}-){10}\w{3}/; 11 | export const CANISTER_REGEX = /(\w{5}-){4}\w{3}/; 12 | export const CANISTER_MAX_LENGTH = 27; 13 | export const XTC_ID = 'aanaa-xaaaa-aaaah-aaeiq-cai'; 14 | export const ALPHANUM_REGEX = /^[a-zA-Z0-9]+$/; 15 | export const IC_MAINNET_URLS = ['https://mainnet.dfinity.network', 'ic0.app']; -------------------------------------------------------------------------------- /src/utils/dfx/dfx.test.ts: -------------------------------------------------------------------------------- 1 | import { getICPTransactions } from './history/rosetta'; 2 | import { 3 | mockRosettaTransaction, 4 | mockTransactionResult, 5 | mockAccountID, 6 | } from './mockData'; 7 | 8 | jest.mock('cross-fetch', () => 9 | jest.fn().mockImplementation(() => mockRosettaTransaction) 10 | ); 11 | 12 | describe('DFX Utils', () => { 13 | describe('rosetta', () => { 14 | describe('getTransactions', () => { 15 | it('get correct info', async () => { 16 | expect(await getICPTransactions(mockAccountID)).toMatchObject( 17 | mockTransactionResult 18 | ); 19 | }); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/utils/dfx/history/cap/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | /* eslint-disable no-underscore-dangle */ 3 | import axios, { AxiosResponse, Method } from 'axios'; 4 | import { 5 | prettifyCapTransactions, 6 | TransactionPrettified, 7 | } from '@psychedelic/cap-js'; 8 | import { getTokens, getAllNFTS, TokenRegistry } from '@psychedelic/dab-js'; 9 | 10 | import { getCanisterInfo } from '../../../dab'; 11 | import { parsePrincipal } from '../../../object'; 12 | import { 13 | GetCapTransactionsParams, 14 | GetUserTransactionResponse, 15 | LastEvaluatedKey, 16 | } from '../../../../interfaces/cap'; 17 | import { SONIC_SWAP_CANISTER_ID } from './sonic'; 18 | import { HttpAgent } from '@dfinity/agent'; 19 | import { formatCapTransaction } from '../../../formatter/transaction/capTransactionFormatter'; 20 | 21 | 22 | const parseUnderscoreNumber = value => 23 | value ? Number(value.replace('_', '')) : null; 24 | 25 | const getTransactionCanister = (contract: string): string | undefined => 26 | contract?.split('#')?.[1]; 27 | 28 | const metadataArrayToObject = metadata => 29 | metadata.reduce( 30 | (acum, item) => ({ ...acum, [item.principal_id.toString()]: item }), 31 | {} 32 | ); 33 | 34 | const getCanistersInfo = async ( 35 | canisterIds: string[], 36 | agent?: HttpAgent 37 | ): Promise => { 38 | const dabTokensInfo = metadataArrayToObject(await getTokens({ agent })); 39 | const dabNFTsInfo = metadataArrayToObject(await getAllNFTS({ agent })); 40 | const dabInfo = await Promise.all( 41 | canisterIds.map(async canisterId => { 42 | let canisterInfo = { canisterId }; 43 | // First check if present in nft or token registries 44 | if (dabTokensInfo[canisterId]) 45 | canisterInfo['tokenRegistryInfo'] = dabTokensInfo[canisterId]; 46 | if (dabNFTsInfo[canisterId]) 47 | canisterInfo['nftRegistryInfo'] = dabNFTsInfo[canisterId]; 48 | try { 49 | // Fetch extra metadata from canister registry 50 | const fetchedCanisterInfo = await getCanisterInfo(canisterId, agent); 51 | canisterInfo = { ...canisterInfo, ...fetchedCanisterInfo }; 52 | } catch (error) { 53 | /* eslint-disable-next-line */ 54 | console.error('DAB error: ', error); 55 | } 56 | return canisterInfo; 57 | }) 58 | ); 59 | const canistersInfo = dabInfo.reduce( 60 | (acum, info) => ({ ...acum, [info.canisterId]: info }), 61 | {} 62 | ); 63 | return canistersInfo; 64 | }; 65 | 66 | const getCanisterIdsFromSonicTx = details => { 67 | const { to, from, token0, token1, token, token_id, token_identifier } = 68 | details || {}; 69 | 70 | const tokenId = 71 | details?.tokenId || 72 | token || 73 | token_id || 74 | parseUnderscoreNumber(token_identifier) || 75 | undefined; 76 | 77 | return [to, from, token0, token1, tokenId]; 78 | }; 79 | 80 | const getCanisterIds = ( 81 | prettyEvents: { 82 | canisterId: string; 83 | prettyEvent: TransactionPrettified; 84 | }[] 85 | ) => { 86 | const canisterIds = prettyEvents.reduce( 87 | (acum, { canisterId, prettyEvent }) => { 88 | if (canisterId === SONIC_SWAP_CANISTER_ID) { 89 | return acum 90 | .concat([canisterId]) 91 | .concat(getCanisterIdsFromSonicTx(prettyEvent.details)); 92 | } else { 93 | return acum.concat([canisterId]); 94 | } 95 | }, 96 | [] as string[] 97 | ); 98 | return [...new Set(canisterIds.map((id) => parsePrincipal(id)))].filter(value => value); 99 | }; 100 | 101 | export const getCapTransactions = async ({ 102 | principalId, 103 | lastEvaluatedKey, 104 | agent, 105 | }: GetCapTransactionsParams): Promise => { 106 | let total: number = 0; 107 | let events: { sk: string, canisterId: string, prettyEvent: TransactionPrettified }[] = []; 108 | let LastEvaluatedKey: LastEvaluatedKey | undefined = lastEvaluatedKey; 109 | try { 110 | do { 111 | const options = { 112 | method: 'get' as Method, 113 | url: `https://kyasshu.fleek.co/cap/user/txns/${principalId}`, 114 | ...(LastEvaluatedKey 115 | ? { 116 | params: { 117 | LastEvaluatedKey, 118 | }, 119 | } 120 | : {}), 121 | }; 122 | const response = await axios(options); 123 | const prettifyEvents = response.data.Items.map(item => ({ 124 | sk: item.sk, 125 | canisterId: getTransactionCanister(item.contractId), 126 | prettyEvent: prettifyCapTransactions(item.event), 127 | })); 128 | LastEvaluatedKey = response.data.LastEvaluatedKey; 129 | total += response.data.Count; 130 | events = [...events, ...prettifyEvents]; 131 | } while (LastEvaluatedKey); 132 | } catch (e) { 133 | console.error('CAP transactions error:', e); 134 | return { 135 | total: 0, 136 | transactions: [], 137 | }; 138 | } 139 | // Keep only last 50 txs by timestamp 140 | const lastEvents = events.sort( 141 | (a, b) => (a.prettyEvent.time < b.prettyEvent.time) 142 | ? -1 143 | : ((a.prettyEvent.time > b.prettyEvent.time) 144 | ? 1 145 | : 0 146 | ), 147 | ).slice(-50); 148 | const canistersInfo = await getCanistersInfo(getCanisterIds(lastEvents), agent); 149 | const transactions = await Promise.all( 150 | lastEvents.map(async prettyEvent => 151 | formatCapTransaction(prettyEvent, canistersInfo) 152 | ) 153 | ); 154 | return { 155 | total: total >= 50 ? 50 : total, 156 | transactions, 157 | }; 158 | }; 159 | 160 | export default {}; 161 | -------------------------------------------------------------------------------- /src/utils/dfx/history/cap/sonic.ts: -------------------------------------------------------------------------------- 1 | import { TokenRegistry } from '@psychedelic/dab-js'; 2 | import { parsePrincipal } from '../../../object'; 3 | 4 | export const SONIC_SWAP_CANISTER_ID = '3xwpq-ziaaa-aaaah-qcn4a-cai'; 5 | 6 | const getHandledTokenInfo = async (principal, canistersInfo) => { 7 | if (!principal) return; 8 | const canisterId = parsePrincipal(principal); 9 | if (canistersInfo[canisterId]?.tokenRegistryInfo) 10 | return canistersInfo[canisterId]?.tokenRegistryInfo; 11 | else { 12 | const registry = new TokenRegistry(); 13 | const data = await registry.get(canisterId); 14 | return data; 15 | } 16 | }; 17 | 18 | export const buildSonicData = async ({ 19 | canisterId, 20 | details, 21 | operation, 22 | canistersInfo, 23 | tokenId, 24 | }) => { 25 | if (canisterId !== SONIC_SWAP_CANISTER_ID) return; 26 | const { amount, amountIn, amountOut } = details || {}; 27 | const isSwap = operation?.toLowerCase?.()?.includes?.('swap'); 28 | const isLiquidity = operation?.toLowerCase?.()?.includes?.('liquidity'); 29 | 30 | let data: any = { 31 | token: canistersInfo[tokenId]?.tokenRegistryInfo, 32 | amount, 33 | }; 34 | 35 | const formatSwapData = (data) => (data?.tokenRegistryInfo || { 36 | name: data?.name, 37 | thumbnail: data?.logo_url, 38 | logo: data?.logo_url, 39 | description: data?.description, 40 | }); 41 | 42 | const from = formatSwapData(canistersInfo[details?.from]); 43 | const to = formatSwapData(canistersInfo[details?.to]); 44 | 45 | if (isSwap) { 46 | data.swap = { 47 | from, 48 | to, 49 | amountIn, 50 | amountOut, 51 | }; 52 | delete data.amount; 53 | } 54 | if (isLiquidity) { 55 | const token0 = canistersInfo[details?.token0]?.tokenRegistryInfo; 56 | const token1 = canistersInfo[details?.token1]?.tokenRegistryInfo; 57 | const pair = `${token0?.details?.symbol}/${token1?.details?.symbol}`; 58 | data.liquidity = { 59 | pair, 60 | token0: { token: token0, amount: details?.amount0 }, 61 | token1: { token: token1, amount: details?.amount1 }, 62 | }; 63 | delete data.amount; 64 | } 65 | return data; 66 | }; 67 | -------------------------------------------------------------------------------- /src/utils/dfx/history/rosetta.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | /* eslint-disable @typescript-eslint/camelcase */ 3 | import fetch from 'cross-fetch'; 4 | import { TOKENS } from '../../../constants/tokens'; 5 | import { ERRORS } from '../../../errors'; 6 | import { NET_ID, ROSETTA_URL } from '../constants'; 7 | 8 | import { GetTransactionsResponse } from '../../../interfaces/transactions'; 9 | import { formatIcpTransaccion } from '../../formatter/transaction/ipcTransactionFormatter'; 10 | 11 | export const MILI_PER_SECOND = 1000000; 12 | 13 | interface Balance { 14 | value: string; 15 | decimals: number; 16 | error?: string; 17 | }; 18 | 19 | export const getICPTransactions = async ( 20 | accountId: string 21 | ): Promise => { 22 | const response = await fetch(`${ROSETTA_URL}/search/transactions`, { 23 | method: 'POST', 24 | body: JSON.stringify({ 25 | network_identifier: NET_ID, 26 | account_identifier: { 27 | address: accountId, 28 | }, 29 | }), 30 | headers: { 31 | 'Content-Type': 'application/json', 32 | Accept: '*/*', 33 | }, 34 | }); 35 | if (!response.ok) 36 | throw Error(`${ERRORS.GET_TRANSACTIONS_FAILS} ${response.statusText}`); 37 | const { transactions, total_count } = await response.json(); 38 | const transactionsInfo = transactions.map(({ transaction }) => 39 | formatIcpTransaccion(accountId, transaction) 40 | ); 41 | return { 42 | total: total_count, 43 | transactions: transactionsInfo, 44 | }; 45 | }; 46 | 47 | export const getICPBalance = async (accountId: string): Promise => { 48 | const response = await fetch(`${ROSETTA_URL}/account/balance`, { 49 | method: 'POST', 50 | body: JSON.stringify({ 51 | network_identifier: NET_ID, 52 | account_identifier: { 53 | address: accountId, 54 | }, 55 | }), 56 | headers: { 57 | 'Content-Type': 'application/json', 58 | Accept: '*/*', 59 | }, 60 | }); 61 | if (!response.ok) { 62 | return { value: 'Error', decimals: TOKENS.ICP.decimals, error: response.statusText }; 63 | } 64 | const { balances } = await response.json(); 65 | const [{ value, currency }] = balances; 66 | return { value, decimals: currency.decimals }; 67 | }; 68 | 69 | export default {}; 70 | -------------------------------------------------------------------------------- /src/utils/dfx/history/xtcHistory.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | /* eslint-disable no-underscore-dangle */ 3 | import axios from 'axios'; 4 | 5 | import { GetTransactionsResponse } from '../../../interfaces/transactions'; 6 | import { formatXTCTransaction } from '../../formatter/transaction/xtcTransactionFormatter'; 7 | 8 | const KYASHU_URL = 'https://kyasshu.fleek.co'; 9 | 10 | export const getXTCTransactions = async ( 11 | principalId: string, 12 | txnIds?: Array 13 | ): Promise => { 14 | const url = `${KYASHU_URL}/txns/${principalId}${ 15 | txnIds?.length ? `?txnIds=[${txnIds.join(',')}]` : '' 16 | }`; 17 | try { 18 | const response = await axios.get(url); 19 | return { 20 | total: response.data.length, 21 | transactions: response.data.map(transaction => 22 | formatXTCTransaction(principalId, transaction) 23 | ), 24 | } as GetTransactionsResponse; 25 | } catch (e) { 26 | console.log('getXTCTransactions error', e); 27 | return { 28 | total: 0, 29 | transactions: [], 30 | }; 31 | } 32 | }; 33 | 34 | export const requestCacheUpdate = async ( 35 | principalId: string, 36 | txnIds?: Array 37 | ): Promise => { 38 | try { 39 | const response = await axios.post(`${KYASHU_URL}/txn/${principalId}`, { 40 | pid: principalId, 41 | txnIds: txnIds?.map(tx => tx.toString()), 42 | }); 43 | return !!response.data; 44 | } catch (e) { 45 | console.log('kyasshuu error', e); 46 | return false; 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /src/utils/dfx/icns/index.ts: -------------------------------------------------------------------------------- 1 | import { HttpAgent, Actor, ActorSubclass } from "@dfinity/agent"; 2 | 3 | 4 | import resolverIDL from '../../../idls/icns_resolver.did'; 5 | import registryIDL from '../../../idls/icns_registry.did'; 6 | import reverseRegistrarIDL from '../../../idls/icns_reverse_registrar.did'; 7 | import Resolver, { DefaultInfoExt } from '../../../interfaces/icns_resolver'; 8 | import Registry, { RecordExt } from '../../../interfaces/icns_registry'; 9 | import ReverseRegistrar from '../../../interfaces/icns_reverse_registrar'; 10 | import { Principal } from "@dfinity/principal"; 11 | import { NFTCollection, standards } from "@psychedelic/dab-js"; 12 | import { ERRORS } from "../../../errors"; 13 | 14 | const ICNS_REGISTRY_ID = 'e5kvl-zyaaa-aaaan-qabaq-cai'; 15 | const ICNS_RESOLVER_ID = 'euj6x-pqaaa-aaaan-qabba-cai'; 16 | const ICNS_REVERSE_REGISTRAR_ID = 'etiyd-ciaaa-aaaan-qabbq-cai'; 17 | const ICNS_IMG = 'https://icns.id/Rectangle.jpg'; 18 | const ICNS_LOGO = 'https://icns.id/ICNS-logo.png'; 19 | 20 | export default class ICNSAdapter { 21 | #resolver: ActorSubclass; 22 | #registry: ActorSubclass; 23 | #reverseRegistrar: ActorSubclass; 24 | #agent: HttpAgent; 25 | constructor(agent: HttpAgent) { 26 | this.#agent = agent; 27 | this.#resolver = Actor.createActor(resolverIDL, { 28 | canisterId: ICNS_RESOLVER_ID, 29 | agent, 30 | }); 31 | 32 | this.#registry = Actor.createActor(registryIDL, { 33 | canisterId: ICNS_REGISTRY_ID, 34 | agent, 35 | }); 36 | 37 | this.#reverseRegistrar = Actor.createActor(reverseRegistrarIDL, { 38 | canisterId: ICNS_REVERSE_REGISTRAR_ID, 39 | agent, 40 | }); 41 | } 42 | 43 | public resolveName = async (name: string, isICP: boolean): Promise => { 44 | let record: [] | [DefaultInfoExt] | [RecordExt] = await this.#resolver.getUserDefaultInfo(name); 45 | const { icp, pid: principal } = record?.[0] || {}; 46 | const accountId = icp?.[0]; 47 | if (isICP && accountId) { 48 | return accountId; 49 | } 50 | if (!principal) { 51 | record = await this.#registry.getRecord(name); 52 | const { owner } = record?.[0] || {}; 53 | return owner?.toString?.(); 54 | } 55 | return principal?.toString?.(); 56 | }; 57 | 58 | public getICNSNames = async (): Promise => { 59 | const _names = await this.#registry.getUserDomains(await this.#agent.getPrincipal()); 60 | const names = _names?.[0] || []; 61 | return names; 62 | }; 63 | 64 | public getICNSCollection = async (): Promise => { 65 | let icnsNames = await this.getICNSNames(); 66 | const formattedNames = icnsNames?.map( 67 | (name) => ({ 68 | name: name?.name?.toString() || '', 69 | index: name?.id?.toString(), 70 | url: ICNS_IMG, 71 | collection: 'ICNS', 72 | desc: 'ICNS Name Record', 73 | canister: ICNS_REGISTRY_ID, 74 | metadata: {}, 75 | standard: standards.NFT.dip721, 76 | }), 77 | ); 78 | return { 79 | canisterId: ICNS_REGISTRY_ID, 80 | description: 'ICNS .icp names', 81 | icon: ICNS_LOGO, 82 | name: 'ICNS', 83 | standard: standards.NFT.dip721, 84 | tokens: formattedNames || [], 85 | }; 86 | }; 87 | 88 | public getICNSReverseResolvedName = async (principalId?: string): Promise => { 89 | const ownPrincipal = await this.#agent.getPrincipal(); 90 | const principal = principalId ? Principal.from(principalId) : ownPrincipal; 91 | const name = await this.#reverseRegistrar.getName(principal); 92 | return name; 93 | } 94 | 95 | public setICNSReverseResolvedName = async (name: string): Promise => { 96 | await this.resetNameRecordData(name); 97 | const result = await this.#reverseRegistrar.setName(name); 98 | if ('ok' in result) { 99 | return result.ok; 100 | } 101 | throw(ERRORS.ICNS_REVERSE_RESOLVER_ERROR); 102 | } 103 | 104 | public getICNSMappings = async (principalIds: string[]): Promise<{ [key: string]: string | undefined }> => { 105 | const mappings = {}; 106 | await Promise.all(principalIds.map(async pid => { 107 | try { 108 | const name = await this.getICNSReverseResolvedName(pid); 109 | if (name) { 110 | mappings[name] = pid 111 | mappings[pid] = name; 112 | } 113 | } catch (e) { 114 | console.log('error getting ICNS mapping', pid, e); 115 | } 116 | })); 117 | return mappings; 118 | } 119 | 120 | public resetNameRecordData = async (name: string): Promise => { 121 | try { 122 | const principal = await this.#agent.getPrincipal(); 123 | await this.#resolver.setAddr(name, 'icp.principal', [principal.toString()]); 124 | } catch (e) { 125 | console.log('Error when reseting your name data', e); 126 | } 127 | } 128 | }; 129 | -------------------------------------------------------------------------------- /src/utils/dfx/icns/utils.ts: -------------------------------------------------------------------------------- 1 | import { InferredTransaction } from '../../../interfaces/transactions'; 2 | import { PRINCIPAL_REGEX } from '../constants'; 3 | 4 | interface ICNSMapping { [key: string]: string | undefined } 5 | 6 | export const getMappingValue = (pid: string, mappings: ICNSMapping) => ({ principal: pid, icns: mappings[pid] }); 7 | 8 | export const replacePrincipalsForICNS = (tx: InferredTransaction, mappings: ICNSMapping): InferredTransaction => { 9 | const parsedTx = { ...tx }; 10 | const { from: detailFrom, to } = parsedTx?.details || {}; 11 | const from = detailFrom || parsedTx?.caller; 12 | parsedTx.details = { 13 | ...parsedTx.details, 14 | from: getMappingValue(from, mappings), 15 | to: getMappingValue(to, mappings), 16 | }; 17 | return parsedTx; 18 | } 19 | 20 | export const recursiveFindPrincipals = (transactions: InferredTransaction[]): string[] => { 21 | return transactions.reduce((acc, tx) => { 22 | const copy: string[] = [...acc]; 23 | const { from, to } = tx.details || {}; 24 | if (PRINCIPAL_REGEX.test(from)) copy.push(from); 25 | if (PRINCIPAL_REGEX.test(to)) copy.push(to); 26 | return [...new Set(copy)]; 27 | }, []); 28 | } -------------------------------------------------------------------------------- /src/utils/dfx/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | /* eslint-disable @typescript-eslint/camelcase */ 3 | /* eslint-disable camelcase */ 4 | import { AnonymousIdentity, HttpAgent } from '@dfinity/agent'; 5 | import { BinaryBlob, blobFromUint8Array } from '@dfinity/candid'; 6 | import { SignIdentity } from '@dfinity/agent'; 7 | import crossFetch from 'cross-fetch'; 8 | 9 | import Secp256k1KeyIdentity from '../identity/secpk256k1/identity'; 10 | import { wrappedFetch } from './wrappedFetch'; 11 | import { IC_MAINNET_URLS, PLUG_PROXY_HOST } from './constants'; 12 | import { ERRORS } from '../../errors'; 13 | 14 | export interface CreateAgentArgs { 15 | secretKey?: BinaryBlob; 16 | defaultIdentity?: SignIdentity; 17 | fetch?: any; 18 | host?: string; 19 | wrapped?: boolean, 20 | } 21 | 22 | export const createIdentity = (secretKey: BinaryBlob): Secp256k1KeyIdentity => 23 | Secp256k1KeyIdentity.fromSecretKey(secretKey); 24 | 25 | export const createAgent = ({ 26 | secretKey, 27 | defaultIdentity, 28 | fetch = crossFetch, 29 | host, 30 | wrapped = true, 31 | }: CreateAgentArgs): HttpAgent => { 32 | if (!defaultIdentity && !secretKey) throw new Error(ERRORS.EMPTY_IDENTITY_ERROR); 33 | const identity = 34 | defaultIdentity || (secretKey ? createIdentity(blobFromUint8Array(secretKey)) : new AnonymousIdentity() ); 35 | const agent = new HttpAgent({ 36 | host: (wrapped ? PLUG_PROXY_HOST : host)|| PLUG_PROXY_HOST, 37 | fetch: wrapped ? wrappedFetch(fetch) : fetch, 38 | identity, 39 | }); 40 | if (host && !IC_MAINNET_URLS.includes(host)) { 41 | agent.fetchRootKey(); 42 | } 43 | return agent; 44 | }; 45 | 46 | export { createNNSActor } from './nns_uid'; 47 | -------------------------------------------------------------------------------- /src/utils/dfx/mockData.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/class-name-casing */ 2 | /* eslint-disable camelcase */ 3 | /* eslint-disable @typescript-eslint/camelcase */ 4 | export const mockRosettaTransaction = { 5 | ok: true, 6 | json: () => ({ 7 | transactions: [ 8 | { 9 | block_identifier: { 10 | index: 163263, 11 | hash: 12 | 'd5b368a452a20f20ba7ea284f63da3b0880853f27652c593f7f279d1a85e02eb', 13 | }, 14 | transaction: { 15 | transaction_identifier: { 16 | hash: 17 | '77d3b28f4e6b7aea6c012ff7707b21b8b398f5146053a6038b2ff02b983bba2e', 18 | }, 19 | operations: [ 20 | { 21 | operation_identifier: { index: 0 }, 22 | type: 'TRANSACTION', 23 | status: 'COMPLETED', 24 | account: { 25 | address: 26 | '4dfa940def17f1427ae47378c440f10185867677109a02bc8374fc25b9dee8af', 27 | }, 28 | amount: { 29 | value: '-20000000', 30 | currency: { symbol: 'ICP', decimals: 8 }, 31 | }, 32 | }, 33 | { 34 | operation_identifier: { index: 1 }, 35 | type: 'TRANSACTION', 36 | status: 'COMPLETED', 37 | account: { 38 | address: 39 | '3cbd622655d0496c6e28398f5d6889c45fab26d22dcc735da6832f867fd290a3', 40 | }, 41 | amount: { 42 | value: '20000000', 43 | currency: { symbol: 'ICP', decimals: 8 }, 44 | }, 45 | }, 46 | { 47 | operation_identifier: { index: 2 }, 48 | type: 'FEE', 49 | status: 'COMPLETED', 50 | account: { 51 | address: 52 | '4dfa940def17f1427ae47378c440f10185867677109a02bc8374fc25b9dee8af', 53 | }, 54 | amount: { 55 | value: '-10000', 56 | currency: { symbol: 'ICP', decimals: 8 }, 57 | }, 58 | }, 59 | ], 60 | metadata: { 61 | block_height: 163263, 62 | memo: 17658957290334035096, 63 | timestamp: 1623432025640774091, 64 | }, 65 | }, 66 | }, 67 | { 68 | block_identifier: { 69 | index: 170497, 70 | hash: 71 | '0112c7ae622a3542dc5e2642367c685eee983082d0ac11d21a1f03d4ea0045df', 72 | }, 73 | transaction: { 74 | transaction_identifier: { 75 | hash: 76 | 'b288ebc8facbacbb3fb3b0e2864c99feaf3898bea8c33d6a1ecc793d2177ded3', 77 | }, 78 | operations: [ 79 | { 80 | operation_identifier: { index: 0 }, 81 | type: 'TRANSACTION', 82 | status: 'COMPLETED', 83 | account: { 84 | address: 85 | '3cbd622655d0496c6e28398f5d6889c45fab26d22dcc735da6832f867fd290a3', 86 | }, 87 | amount: { 88 | value: '-10000', 89 | currency: { symbol: 'ICP', decimals: 8 }, 90 | }, 91 | }, 92 | { 93 | operation_identifier: { index: 1 }, 94 | type: 'TRANSACTION', 95 | status: 'COMPLETED', 96 | account: { 97 | address: 98 | '4dfa940def17f1427ae47378c440f10185867677109a02bc8374fc25b9dee8af', 99 | }, 100 | amount: { 101 | value: '10000', 102 | currency: { symbol: 'ICP', decimals: 8 }, 103 | }, 104 | }, 105 | { 106 | operation_identifier: { index: 2 }, 107 | type: 'FEE', 108 | status: 'COMPLETED', 109 | account: { 110 | address: 111 | '3cbd622655d0496c6e28398f5d6889c45fab26d22dcc735da6832f867fd290a3', 112 | }, 113 | amount: { 114 | value: '-10000', 115 | currency: { symbol: 'ICP', decimals: 8 }, 116 | }, 117 | }, 118 | ], 119 | metadata: { 120 | block_height: 170497, 121 | memo: 1085832164, 122 | timestamp: 1623531693580114606, 123 | }, 124 | }, 125 | }, 126 | ], 127 | total_count: 2, 128 | }), 129 | }; 130 | 131 | export const mockXTCTransactions = [ 132 | { 133 | txnId: '1166', 134 | event: { 135 | cycles: 1, 136 | kind: { 137 | Transfer: { 138 | from: { 139 | _isPrincipal: true, 140 | _arr: { 141 | '0': 238, 142 | '1': 182, 143 | '2': 122, 144 | '3': 249, 145 | '4': 253, 146 | '5': 205, 147 | '6': 1, 148 | '7': 244, 149 | '8': 188, 150 | '9': 239, 151 | '10': 103, 152 | '11': 22, 153 | '12': 0, 154 | '13': 44, 155 | '14': 40, 156 | '15': 143, 157 | '16': 218, 158 | '17': 132, 159 | '18': 81, 160 | '19': 94, 161 | '20': 79, 162 | '21': 37, 163 | '22': 87, 164 | '23': 113, 165 | '24': 63, 166 | '25': 113, 167 | '26': 175, 168 | '27': 117, 169 | '28': 2, 170 | }, 171 | }, 172 | to: { 173 | _isPrincipal: true, 174 | _arr: { 175 | '0': 233, 176 | '1': 136, 177 | '2': 172, 178 | '3': 214, 179 | '4': 124, 180 | '5': 158, 181 | '6': 0, 182 | '7': 138, 183 | '8': 92, 184 | '9': 99, 185 | '10': 118, 186 | '11': 115, 187 | '12': 167, 188 | '13': 109, 189 | '14': 45, 190 | '15': 164, 191 | '16': 10, 192 | '17': 190, 193 | '18': 103, 194 | '19': 105, 195 | '20': 88, 196 | '21': 27, 197 | '22': 50, 198 | '23': 87, 199 | '24': 26, 200 | '25': 160, 201 | '26': 234, 202 | '27': 125, 203 | '28': 2, 204 | }, 205 | }, 206 | }, 207 | }, 208 | fee: 0, 209 | timestamp: 1629386839664, 210 | }, 211 | }, 212 | { 213 | txnId: '1167', 214 | event: { 215 | cycles: 1, 216 | kind: { 217 | Transfer: { 218 | from: { 219 | _isPrincipal: true, 220 | _arr: { 221 | '0': 238, 222 | '1': 182, 223 | '2': 122, 224 | '3': 249, 225 | '4': 253, 226 | '5': 205, 227 | '6': 1, 228 | '7': 244, 229 | '8': 188, 230 | '9': 239, 231 | '10': 103, 232 | '11': 22, 233 | '12': 0, 234 | '13': 44, 235 | '14': 40, 236 | '15': 143, 237 | '16': 218, 238 | '17': 132, 239 | '18': 81, 240 | '19': 94, 241 | '20': 79, 242 | '21': 37, 243 | '22': 87, 244 | '23': 113, 245 | '24': 63, 246 | '25': 113, 247 | '26': 175, 248 | '27': 117, 249 | '28': 2, 250 | }, 251 | }, 252 | to: { 253 | _isPrincipal: true, 254 | _arr: { 255 | '0': 233, 256 | '1': 136, 257 | '2': 172, 258 | '3': 214, 259 | '4': 124, 260 | '5': 158, 261 | '6': 0, 262 | '7': 138, 263 | '8': 92, 264 | '9': 99, 265 | '10': 118, 266 | '11': 115, 267 | '12': 167, 268 | '13': 109, 269 | '14': 45, 270 | '15': 164, 271 | '16': 10, 272 | '17': 190, 273 | '18': 103, 274 | '19': 105, 275 | '20': 88, 276 | '21': 27, 277 | '22': 50, 278 | '23': 87, 279 | '24': 26, 280 | '25': 160, 281 | '26': 234, 282 | '27': 125, 283 | '28': 2, 284 | }, 285 | }, 286 | }, 287 | }, 288 | fee: 0, 289 | timestamp: 1629386855226, 290 | }, 291 | }, 292 | { 293 | txnId: '1168', 294 | event: { 295 | cycles: 1, 296 | kind: { 297 | Transfer: { 298 | from: { 299 | _isPrincipal: true, 300 | _arr: { 301 | '0': 238, 302 | '1': 182, 303 | '2': 122, 304 | '3': 249, 305 | '4': 253, 306 | '5': 205, 307 | '6': 1, 308 | '7': 244, 309 | '8': 188, 310 | '9': 239, 311 | '10': 103, 312 | '11': 22, 313 | '12': 0, 314 | '13': 44, 315 | '14': 40, 316 | '15': 143, 317 | '16': 218, 318 | '17': 132, 319 | '18': 81, 320 | '19': 94, 321 | '20': 79, 322 | '21': 37, 323 | '22': 87, 324 | '23': 113, 325 | '24': 63, 326 | '25': 113, 327 | '26': 175, 328 | '27': 117, 329 | '28': 2, 330 | }, 331 | }, 332 | to: { 333 | _isPrincipal: true, 334 | _arr: { 335 | '0': 233, 336 | '1': 136, 337 | '2': 172, 338 | '3': 214, 339 | '4': 124, 340 | '5': 158, 341 | '6': 0, 342 | '7': 138, 343 | '8': 92, 344 | '9': 99, 345 | '10': 118, 346 | '11': 115, 347 | '12': 167, 348 | '13': 109, 349 | '14': 45, 350 | '15': 164, 351 | '16': 10, 352 | '17': 190, 353 | '18': 103, 354 | '19': 105, 355 | '20': 88, 356 | '21': 27, 357 | '22': 50, 358 | '23': 87, 359 | '24': 26, 360 | '25': 160, 361 | '26': 234, 362 | '27': 125, 363 | '28': 2, 364 | }, 365 | }, 366 | }, 367 | }, 368 | fee: 0, 369 | timestamp: 1629386887795, 370 | }, 371 | }, 372 | { 373 | txnId: '1169', 374 | event: { 375 | cycles: 1, 376 | kind: { 377 | Transfer: { 378 | from: { 379 | _isPrincipal: true, 380 | _arr: { 381 | '0': 238, 382 | '1': 182, 383 | '2': 122, 384 | '3': 249, 385 | '4': 253, 386 | '5': 205, 387 | '6': 1, 388 | '7': 244, 389 | '8': 188, 390 | '9': 239, 391 | '10': 103, 392 | '11': 22, 393 | '12': 0, 394 | '13': 44, 395 | '14': 40, 396 | '15': 143, 397 | '16': 218, 398 | '17': 132, 399 | '18': 81, 400 | '19': 94, 401 | '20': 79, 402 | '21': 37, 403 | '22': 87, 404 | '23': 113, 405 | '24': 63, 406 | '25': 113, 407 | '26': 175, 408 | '27': 117, 409 | '28': 2, 410 | }, 411 | }, 412 | to: { 413 | _isPrincipal: true, 414 | _arr: { 415 | '0': 233, 416 | '1': 136, 417 | '2': 172, 418 | '3': 214, 419 | '4': 124, 420 | '5': 158, 421 | '6': 0, 422 | '7': 138, 423 | '8': 92, 424 | '9': 99, 425 | '10': 118, 426 | '11': 115, 427 | '12': 167, 428 | '13': 109, 429 | '14': 45, 430 | '15': 164, 431 | '16': 10, 432 | '17': 190, 433 | '18': 103, 434 | '19': 105, 435 | '20': 88, 436 | '21': 27, 437 | '22': 50, 438 | '23': 87, 439 | '24': 26, 440 | '25': 160, 441 | '26': 234, 442 | '27': 125, 443 | '28': 2, 444 | }, 445 | }, 446 | }, 447 | }, 448 | fee: 0, 449 | timestamp: 1629386903423, 450 | }, 451 | }, 452 | { 453 | txnId: '1170', 454 | event: { 455 | cycles: 1, 456 | kind: { 457 | Transfer: { 458 | from: { 459 | _isPrincipal: true, 460 | _arr: { 461 | '0': 238, 462 | '1': 182, 463 | '2': 122, 464 | '3': 249, 465 | '4': 253, 466 | '5': 205, 467 | '6': 1, 468 | '7': 244, 469 | '8': 188, 470 | '9': 239, 471 | '10': 103, 472 | '11': 22, 473 | '12': 0, 474 | '13': 44, 475 | '14': 40, 476 | '15': 143, 477 | '16': 218, 478 | '17': 132, 479 | '18': 81, 480 | '19': 94, 481 | '20': 79, 482 | '21': 37, 483 | '22': 87, 484 | '23': 113, 485 | '24': 63, 486 | '25': 113, 487 | '26': 175, 488 | '27': 117, 489 | '28': 2, 490 | }, 491 | }, 492 | to: { 493 | _isPrincipal: true, 494 | _arr: { 495 | '0': 233, 496 | '1': 136, 497 | '2': 172, 498 | '3': 214, 499 | '4': 124, 500 | '5': 158, 501 | '6': 0, 502 | '7': 138, 503 | '8': 92, 504 | '9': 99, 505 | '10': 118, 506 | '11': 115, 507 | '12': 167, 508 | '13': 109, 509 | '14': 45, 510 | '15': 164, 511 | '16': 10, 512 | '17': 190, 513 | '18': 103, 514 | '19': 105, 515 | '20': 88, 516 | '21': 27, 517 | '22': 50, 518 | '23': 87, 519 | '24': 26, 520 | '25': 160, 521 | '26': 234, 522 | '27': 125, 523 | '28': 2, 524 | }, 525 | }, 526 | }, 527 | }, 528 | fee: 0, 529 | timestamp: 1629386947808, 530 | }, 531 | }, 532 | ]; 533 | 534 | export const mockTransactionResult = { 535 | total: 2, 536 | transactions: [ 537 | { 538 | details: { 539 | amount: '20000000', 540 | currency: { symbol: 'ICP', decimals: 8 }, 541 | to: '3cbd622655d0496c6e28398f5d6889c45fab26d22dcc735da6832f867fd290a3', 542 | from: 543 | '4dfa940def17f1427ae47378c440f10185867677109a02bc8374fc25b9dee8af', 544 | fee: { 545 | amount: '-10000', 546 | currency: { symbol: 'ICP', decimals: 8 }, 547 | }, 548 | status: 'COMPLETED', 549 | }, 550 | type: 'RECEIVE', 551 | caller: 552 | '4dfa940def17f1427ae47378c440f10185867677109a02bc8374fc25b9dee8af', 553 | hash: '77d3b28f4e6b7aea6c012ff7707b21b8b398f5146053a6038b2ff02b983bba2e', 554 | timestamp: 1623432025640.7742, 555 | }, 556 | { 557 | details: { 558 | status: 'COMPLETED', 559 | fee: { 560 | amount: '-10000', 561 | currency: { symbol: 'ICP', decimals: 8 }, 562 | }, 563 | from: 564 | '3cbd622655d0496c6e28398f5d6889c45fab26d22dcc735da6832f867fd290a3', 565 | amount: '10000', 566 | currency: { symbol: 'ICP', decimals: 8 }, 567 | to: '4dfa940def17f1427ae47378c440f10185867677109a02bc8374fc25b9dee8af', 568 | }, 569 | type: 'SEND', 570 | caller: 571 | '3cbd622655d0496c6e28398f5d6889c45fab26d22dcc735da6832f867fd290a3', 572 | hash: 'b288ebc8facbacbb3fb3b0e2864c99feaf3898bea8c33d6a1ecc793d2177ded3', 573 | timestamp: 1623531693580.1147, 574 | }, 575 | ], 576 | }; 577 | 578 | export const mockAccountID = 579 | '3cbd622655d0496c6e28398f5d6889c45fab26d22dcc735da6832f867fd290a3'; 580 | export default {}; 581 | -------------------------------------------------------------------------------- /src/utils/dfx/nns_uid/index.ts: -------------------------------------------------------------------------------- 1 | import { HttpAgent, ActorSubclass } from '@dfinity/agent'; 2 | 3 | import nnsMethods, { NNSServiceExtended } from './methods'; 4 | import { createExtendedActorClass } from '../actorFactory'; 5 | import { NNS_CANISTER_ID } from '../constants'; 6 | import nnsIDLFactory from '../../../idls/nns_uid.did'; 7 | 8 | export const createNNSActor = ( 9 | agent: HttpAgent 10 | ): ActorSubclass => { 11 | return (new (createExtendedActorClass( 12 | agent, 13 | nnsMethods, 14 | NNS_CANISTER_ID, 15 | nnsIDLFactory 16 | ))() as unknown) as ActorSubclass; 17 | }; 18 | 19 | export default createNNSActor; 20 | -------------------------------------------------------------------------------- /src/utils/dfx/nns_uid/methods.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | /* eslint-disable @typescript-eslint/camelcase */ 3 | /* eslint-disable camelcase */ 4 | import { ActorSubclass } from '@dfinity/agent'; 5 | 6 | import NNSService, { 7 | GetTransactionsResponse, 8 | } from '../../../interfaces/nns_uid'; 9 | import { BaseMethodsExtendedActor } from '../actorFactory'; 10 | 11 | type BaseNNSService = BaseMethodsExtendedActor 12 | export interface NNSServiceExtended extends BaseNNSService { 13 | getTransactions: (accountId: string) => Promise; 14 | } 15 | 16 | const getTransactions = ( 17 | actor: ActorSubclass, 18 | accountId: string 19 | ): Promise => { 20 | return actor._get_transactions({ 21 | account_identifier: accountId, 22 | page_size: 20, 23 | offset: 0, 24 | }); 25 | }; 26 | 27 | export default { getTransactions }; 28 | -------------------------------------------------------------------------------- /src/utils/dfx/wrappedFetch.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import crossFetch from 'cross-fetch'; 3 | 4 | import { IC_URL_HOST } from './constants'; 5 | 6 | /* eslint-disable no-param-reassign */ 7 | const wrappedFetchInternal = ( 8 | fetch, 9 | resolve, 10 | reject, 11 | resource, 12 | ...initArgs 13 | ): void => { 14 | fetch(resource, ...initArgs) 15 | .then(response => { 16 | if (!response.success && !response.ok) { 17 | const fallbackResource = new URL(resource); 18 | fallbackResource.host = IC_URL_HOST; 19 | fetch(fallbackResource, ...initArgs) 20 | .then(resolve) 21 | .catch(reject); 22 | } else { 23 | resolve(response); 24 | } 25 | }) 26 | .catch(reject); 27 | }; 28 | 29 | export const wrappedFetch = (fetch = crossFetch) => ( 30 | ...args: Parameters 31 | ): Promise => { 32 | let reject; 33 | let resolve; 34 | 35 | const promise = new Promise((_resolve, _reject) => { 36 | resolve = _resolve; 37 | reject = _reject; 38 | }); 39 | wrappedFetchInternal(fetch, resolve, reject, ...args); 40 | 41 | return promise as Promise; 42 | }; 43 | 44 | export default { wrappedFetch }; 45 | -------------------------------------------------------------------------------- /src/utils/formatter/constants.ts: -------------------------------------------------------------------------------- 1 | export const ACTIVITY_STATUS = { 2 | COMPLETED: 0, 3 | PENDING: 1, 4 | REVERTED: 2, 5 | }; 6 | 7 | export const TOKENS = { 8 | ICP: { 9 | symbol: 'ICP', 10 | canisterId: 'ryjl3-tyaaa-aaaaa-aaaba-cai', 11 | name: 'ICP', 12 | decimals: 8, 13 | amount: 0, 14 | value: 0, 15 | standard: 'ROSETTA', 16 | }, 17 | XTC: { 18 | symbol: 'XTC', 19 | canisterId: 'aanaa-xaaaa-aaaah-aaeiq-cai', 20 | name: 'Cycles', 21 | decimals: 12, 22 | amount: 0, 23 | value: 0, 24 | standard: 'DIP20', 25 | }, 26 | WICP: { 27 | symbol: 'WICP', 28 | canisterId: 'aanaa-xaaaa-aaaah-aaeiq-cai', 29 | name: 'Wrapped ICP', 30 | decimals: 8, 31 | amount: 0, 32 | value: 0, 33 | standard: 'DIP20', 34 | }, 35 | }; 36 | 37 | 38 | export const AMOUNT_ERROR = 'Error'; 39 | export const USD_PER_TC = 1.42656; 40 | -------------------------------------------------------------------------------- /src/utils/formatter/helpers/amountParser.ts: -------------------------------------------------------------------------------- 1 | import { TOKENS, USD_PER_TC } from '../constants' 2 | 3 | export const formatAssetBySymbol = (_amount, symbol, icpPrice) => { 4 | const amount = Number.isNaN(_amount) ? NaN : parseFloat(_amount); 5 | const icpValue = Number.isNaN(amount) ? NaN : amount * icpPrice; 6 | const tcValue = Number.isNaN(amount) ? NaN : amount * USD_PER_TC; 7 | 8 | return ( 9 | { 10 | ICP: { 11 | amount, 12 | value: icpValue, 13 | symbol: 'ICP', 14 | decimals: 8, 15 | }, 16 | XTC: { 17 | amount, 18 | value: tcValue, 19 | symbol: 'XTC', 20 | decimals: 12, 21 | }, 22 | WTC: { 23 | amount, 24 | value: tcValue, 25 | symbol: 'WTC', 26 | decimals: 12, 27 | }, 28 | WICP: { 29 | amount, 30 | value: icpValue, 31 | symbol: 'WICP', 32 | decimals: 8, 33 | }, 34 | default: { amount }, 35 | }[symbol || 'default'] || { amount } 36 | ); 37 | }; 38 | 39 | export const parseToFloatAmount = (amount, decimals) => { 40 | let amountString = `${amount}`; 41 | let prefix = ''; 42 | 43 | if (amountString[0] === '-') { 44 | prefix = '-'; 45 | amountString = amountString.slice(1, amountString.length); 46 | } 47 | 48 | const difference = decimals - amountString.length; 49 | 50 | if (decimals >= amountString.length) { 51 | const formatedString = '0'.repeat(difference + 1) + amountString; 52 | 53 | return `${prefix + formatedString[0]}.${formatedString.slice(1, formatedString.length)}`; 54 | } 55 | 56 | return `${prefix + amountString.slice(0, Math.abs(difference))}.${amountString.slice(Math.abs(difference))}`; 57 | }; 58 | 59 | export const parseAmount = (transactionObject) => { 60 | const { 61 | amount, currency, token, sonicData, 62 | } = transactionObject; 63 | 64 | const { decimals = TOKENS[sonicData?.token?.details?.symbol]?.decimals } = { ...currency, ...token, ...(sonicData?.token ?? {}) }; 65 | 66 | const parsedAmount = parseToFloatAmount(amount, decimals); 67 | 68 | return parsedAmount; 69 | }; 70 | 71 | export const parseFee = (fee) => { 72 | 73 | if (fee instanceof Object && ('token' in fee || 'currency' in fee)) { 74 | return { 75 | ...fee, 76 | amount: parseAmount(fee), 77 | } 78 | } 79 | return { 80 | amount: fee, 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/utils/formatter/transaction/capTransactionFormatter.ts: -------------------------------------------------------------------------------- 1 | import { lebDecode } from '../../crypto/binary'; 2 | import { InferredTransaction } from '../../../interfaces/transactions'; 3 | import { parsePrincipal, recursiveParseBigint } from '../../object'; 4 | 5 | import { buildSonicData } from '../../dfx/history/cap/sonic'; 6 | 7 | const parseUnderscoreNumber = value => 8 | value ? Number(value.replace('_', '')) : null; 9 | 10 | export const formatCapTransaction = async ( 11 | event, 12 | canistersInfo 13 | ): Promise => { 14 | const { canisterId } = event; 15 | const prettifyEvent = event.prettyEvent; 16 | if (canisterId) { 17 | prettifyEvent.details = { 18 | ...prettifyEvent.details, 19 | canisterInfo: canistersInfo[canisterId], 20 | }; 21 | } 22 | const { details, operation, time, caller } = prettifyEvent || {}; 23 | const { amount, token, token_id, token_identifier } = details || {}; 24 | const parsedAmount = 25 | amount instanceof Array && !amount.some(value => typeof value !== 'number') 26 | ? lebDecode(Uint8Array.from(amount as Array)) 27 | : amount; 28 | const tokenId = 29 | details?.tokenId || 30 | token || 31 | token_id || 32 | parseUnderscoreNumber(token_identifier) || 33 | ''; 34 | const formattedTransaction = { 35 | hash: event.sk, 36 | timestamp: time, 37 | type: operation, 38 | details: { 39 | ...details, 40 | amount: parsedAmount, 41 | canisterId, 42 | tokenId, 43 | to: parsePrincipal(details?.to), 44 | from: parsePrincipal(details?.from), 45 | sonicData: await buildSonicData({ 46 | canisterId, 47 | details, 48 | operation, 49 | canistersInfo, 50 | tokenId, 51 | }), 52 | // mkpData: await buildMKPData(), 53 | }, 54 | caller: parsePrincipal(caller) || '', 55 | }; 56 | 57 | return recursiveParseBigint(formattedTransaction); 58 | }; 59 | -------------------------------------------------------------------------------- /src/utils/formatter/transaction/ipcTransactionFormatter.ts: -------------------------------------------------------------------------------- 1 | import { InferredTransaction, GetTransactionsResponse } from '../../../interfaces/transactions'; 2 | import { TOKENS } from '../../../constants/tokens'; 3 | 4 | export const MILI_PER_SECOND = 1000000; 5 | 6 | interface Operation { 7 | account: { 8 | address: string; 9 | }; 10 | amount: { 11 | value: string; 12 | currency: { 13 | symbol: string; 14 | decimals: number; 15 | }; 16 | }; 17 | status: 'COMPLETED' | 'REVERTED' | 'PENDING'; 18 | type: 'TRANSACTION' | 'FEE'; 19 | } 20 | 21 | interface RosettaTransaction { 22 | metadata: { 23 | block_height: number; 24 | memo: number; 25 | timestamp: number; 26 | lockTime: number; 27 | }; 28 | operations: Operation[]; 29 | transaction_identifier: { hash: string }; 30 | } 31 | 32 | 33 | 34 | export const formatIcpTransaccion = ( 35 | accountId: string, 36 | rosettaTransaction: RosettaTransaction 37 | ): InferredTransaction => { 38 | const { 39 | operations, 40 | metadata: { timestamp }, 41 | transaction_identifier: { hash }, 42 | } = rosettaTransaction; 43 | const transaction: any = { details: { status: 'COMPLETED', fee: {} } }; 44 | operations.forEach(operation => { 45 | const value = BigInt(operation.amount.value); 46 | const amount = value.toString(); 47 | if (operation.type === 'FEE') { 48 | transaction.details.fee.amount = amount; 49 | transaction.details.fee.currency = operation.amount.currency; 50 | return; 51 | } 52 | 53 | if (value >= 0) transaction.details.to = operation.account.address; 54 | if (value <= 0) transaction.details.from = operation.account.address; 55 | 56 | if ( 57 | transaction.details.status === 'COMPLETED' && 58 | operation.status !== 'COMPLETED' 59 | ) 60 | transaction.details.status = operation.status; 61 | 62 | transaction.type = 63 | transaction.details.to === accountId ? 'RECEIVE' : 'SEND'; 64 | transaction.details.amount = amount; 65 | transaction.details.currency = operation.amount.currency; 66 | transaction.details.canisterId = TOKENS.ICP.canisterId; 67 | }); 68 | return { 69 | ...transaction, 70 | caller: transaction.details.from, 71 | hash, 72 | timestamp: timestamp / MILI_PER_SECOND, 73 | } as InferredTransaction; 74 | }; 75 | -------------------------------------------------------------------------------- /src/utils/formatter/transaction/xtcTransactionFormatter.ts: -------------------------------------------------------------------------------- 1 | import { TokenInterfaces } from '@psychedelic/dab-js'; 2 | import { TOKENS } from '../../../constants/tokens'; 3 | 4 | import { InferredTransaction } from '../../../interfaces/transactions'; 5 | 6 | const XTC_DECIMALS = 12; 7 | 8 | interface XtcTransactions { 9 | txnId: string; 10 | event: { 11 | cycles: number; 12 | kind: TokenInterfaces.EventDetail; 13 | fee: number; 14 | timestamp: number; 15 | }; 16 | } 17 | 18 | const formatTransfer = ( 19 | principalId: string, 20 | { event }: XtcTransactions, 21 | details: any 22 | ): InferredTransaction => { 23 | if (!('Transfer' in event.kind)) throw Error(); 24 | const transaction: any = { details }; 25 | transaction.details.from = event.kind.Transfer.from; 26 | transaction.details.to = event.kind.Transfer.to; 27 | transaction.caller = event.kind.Transfer.from; 28 | transaction.type = 29 | transaction.details.to === principalId ? 'RECEIVE' : 'SEND'; 30 | 31 | return transaction as InferredTransaction; 32 | }; 33 | 34 | const formatTransferFrom = ( 35 | principalId: string, 36 | { event }: XtcTransactions, 37 | details: any 38 | ): InferredTransaction => { 39 | if (!('TransferFrom' in event.kind)) throw Error(); 40 | const transaction: any = { details }; 41 | transaction.details.from = event.kind.TransferFrom.from; 42 | transaction.details.to = event.kind.TransferFrom.to; 43 | transaction.caller = event.kind.TransferFrom.caller; 44 | transaction.type = 45 | transaction.details.to === principalId ? 'RECEIVE' : 'SEND'; 46 | 47 | return transaction as InferredTransaction; 48 | }; 49 | 50 | const formatBurn = ( 51 | _principalId: string, 52 | { event }: XtcTransactions, 53 | details: any 54 | ): InferredTransaction => { 55 | if (!('Burn' in event.kind)) throw Error(); 56 | const transaction: any = { details }; 57 | transaction.details.from = event.kind.Burn.from; 58 | transaction.details.to = event.kind.Burn.to; 59 | transaction.caller = event.kind.Burn.from; 60 | transaction.type = 'BURN'; 61 | 62 | return transaction as InferredTransaction; 63 | }; 64 | 65 | const formatApprove = ( 66 | _principalId: string, 67 | { event }: XtcTransactions, 68 | details: any 69 | ): InferredTransaction => { 70 | if (!('Approve' in event.kind)) throw Error(); 71 | const transaction: any = { details }; 72 | transaction.details.from = event.kind.Approve.from; 73 | transaction.details.to = event.kind.Approve.to; 74 | transaction.caller = event.kind.Approve.from; 75 | transaction.type = 'APPROVE'; 76 | 77 | return transaction as InferredTransaction; 78 | }; 79 | 80 | const formatMint = ( 81 | _principalId: string, 82 | { event }: XtcTransactions, 83 | details: any 84 | ): InferredTransaction => { 85 | if (!('Mint' in event.kind)) throw Error(); 86 | const transaction: any = { details }; 87 | transaction.details.from = 'Mint'; 88 | transaction.details.to = event.kind.Mint.to; 89 | transaction.caller = _principalId; 90 | transaction.type = 'MINT'; 91 | 92 | return transaction as InferredTransaction; 93 | }; 94 | 95 | const formatCanisterCalled = ( 96 | _principalId: string, 97 | { event }: XtcTransactions, 98 | details: any 99 | ): InferredTransaction => { 100 | if (!('CanisterCalled' in event.kind)) throw Error(); 101 | const transaction: any = { details }; 102 | transaction.details.from = event.kind.CanisterCalled.from; 103 | transaction.caller = event.kind.CanisterCalled.from; 104 | transaction.details.to = `${event.kind.CanisterCalled.canister}_${event.kind.CanisterCalled.method_name}`; 105 | transaction.type = 'CANISTER_CALLED'; 106 | 107 | return transaction as InferredTransaction; 108 | }; 109 | 110 | const formatCanisterCreated = ( 111 | _principalId: string, 112 | { event }: XtcTransactions, 113 | details: any 114 | ): InferredTransaction => { 115 | if (!('CanisterCreated' in event.kind)) throw Error(); 116 | const transaction: any = { details }; 117 | transaction.details.from = event.kind.CanisterCreated.from; 118 | transaction.caller = event.kind.CanisterCreated.from; 119 | transaction.details.to = event.kind.CanisterCreated.canister; 120 | transaction.type = 'CANISTER_CREATED'; 121 | 122 | return transaction as InferredTransaction; 123 | }; 124 | 125 | export const formatXTCTransaction = ( 126 | principalId: string, 127 | xtcTransaction: XtcTransactions 128 | ): InferredTransaction => { 129 | const transactionEvent = xtcTransaction.event; 130 | const transaction: any = {}; 131 | transaction.hash = xtcTransaction.txnId; 132 | transaction.timestamp = xtcTransaction.event.timestamp; 133 | const details = { 134 | canisterId: TOKENS.XTC.canisterId, 135 | amount: transactionEvent.cycles.toString(), 136 | currency: { symbol: 'XTC', decimals: XTC_DECIMALS }, 137 | fee: { 138 | amount: transactionEvent.fee.toString(), 139 | currency: { symbol: 'XTC', decimals: XTC_DECIMALS }, 140 | }, 141 | status: 'COMPLETED', 142 | }; 143 | switch (Object.keys(transactionEvent.kind)[0]) { 144 | case 'Transfer': 145 | return { 146 | ...transaction, 147 | ...formatTransfer(principalId, xtcTransaction, details), 148 | }; 149 | case 'Burn': 150 | return { 151 | ...transaction, 152 | ...formatBurn(principalId, xtcTransaction, details), 153 | }; 154 | case 'Mint': 155 | return { 156 | ...transaction, 157 | ...formatMint(principalId, xtcTransaction, details), 158 | }; 159 | case 'CanisterCalled': 160 | return { 161 | ...transaction, 162 | ...formatCanisterCalled(principalId, xtcTransaction, details), 163 | }; 164 | case 'CanisterCreated': 165 | return { 166 | ...transaction, 167 | ...formatCanisterCreated(principalId, xtcTransaction, details), 168 | }; 169 | case 'Approve': 170 | return { 171 | ...transaction, 172 | ...formatApprove(principalId, xtcTransaction, details), 173 | }; 174 | case 'TransferFrom': 175 | return { 176 | ...transaction, 177 | ...formatTransferFrom(principalId, xtcTransaction, details), 178 | }; 179 | default: 180 | throw Error; 181 | } 182 | }; 183 | -------------------------------------------------------------------------------- /src/utils/formatter/transactionFormatter.ts: -------------------------------------------------------------------------------- 1 | import { FormattedTransactions, FormattedTransaction } from '../../interfaces/transactions'; 2 | import { ACTIVITY_STATUS } from './constants'; 3 | import { formatAssetBySymbol, parseAmount, parseFee } from './helpers/amountParser'; 4 | 5 | 6 | export const formatTransaction = (transaction, principalId, accountId, network, icpPrice): FormattedTransaction => { 7 | const { 8 | details, hash, canisterInfo, caller, timestamp, 9 | } = transaction || {}; 10 | const { sonicData, fee } = details || {}; 11 | const amount = parseAmount(details); 12 | const getSymbol = () => { 13 | if ('tokenRegistryInfo' in (details?.canisterInfo || [])) return details?.canisterInfo.tokenRegistryInfo.symbol; 14 | if ('nftRegistryInfo' in (details?.canisterInfo || [])) return 'NFT'; 15 | return details?.currency?.symbol ?? sonicData?.token?.details?.symbol ?? ''; 16 | }; 17 | 18 | const isOwnTx = [principalId, accountId].includes(transaction?.caller); 19 | const getType = () => { 20 | const { type } = transaction; 21 | if (type.toUpperCase() === 'TRANSFER') { 22 | return isOwnTx ? 'SEND' : 'RECEIVE'; 23 | } 24 | if (type.toUpperCase() === 'LIQUIDITY') { 25 | return `${type.includes('removeLiquidity') ? 'Remove' : 'Add'} Liquidity`; 26 | } 27 | return type.toUpperCase(); 28 | }; 29 | 30 | const asset = formatAssetBySymbol( 31 | amount, 32 | getSymbol(), 33 | icpPrice, 34 | ); 35 | 36 | const trx = { 37 | ...asset, 38 | type: getType(), 39 | hash, 40 | to: details?.to?.icns ?? details?.to?.principal, 41 | from: (details?.from?.icns ?? details?.from?.principal) || caller, 42 | date: timestamp, 43 | status: ACTIVITY_STATUS[details?.status], 44 | logo: details?.sonicData?.token?.logo || details?.canisterInfo?.icon || '', 45 | symbol: getSymbol(), 46 | canisterId: details?.canisterId || details?.canisterInfo?.canisterId, 47 | canisterInfo: canisterInfo || details?.canisterInfo, 48 | details: { 49 | ...details, 50 | amount, 51 | fee: parseFee(details.fee), 52 | caller, 53 | token: 54 | details?.canisterId && network.tokens.find((token) => token.canisterId === details.canisterId), 55 | }, 56 | }; 57 | return trx; 58 | 59 | }; 60 | 61 | 62 | export const formatTransactions = (transactions, principalId, accountId, network, icpPrice): FormattedTransactions => { 63 | 64 | const parsedTrx = transactions?.map((trx) => formatTransaction(trx,principalId,accountId,network,icpPrice)) || []; 65 | 66 | const sortedTransactions = { 67 | total: parsedTrx.length, 68 | transactions: parsedTrx.sort((a, b) => 69 | b.date - a.date < 0 ? -1 : 1 70 | ), 71 | }; 72 | 73 | return sortedTransactions; 74 | }; 75 | -------------------------------------------------------------------------------- /src/utils/getTokensFromCollection.ts: -------------------------------------------------------------------------------- 1 | import { DABCollection, getUserCollectionTokens } from "@psychedelic/dab-js"; 2 | import { Principal } from '@dfinity/principal'; 3 | 4 | export const getTokensFromCollections = async (customNfts, principal, agent) => { 5 | const destructuredCustomNft = customNfts.map(c => { 6 | return {...c, principal_id: c.canisterId}; 7 | }); 8 | 9 | const collectionWithTokens = destructuredCustomNft.map(async (c) => { 10 | const cDABCollection: DABCollection = { 11 | ...c, 12 | principal_id: Principal.fromText(c.principal_id), 13 | icon: c.icon || '', 14 | description: c.description || '', 15 | } 16 | 17 | const tokens = getUserCollectionTokens(cDABCollection, Principal.fromText(principal), agent); 18 | return tokens 19 | }); 20 | 21 | const resultCollectionWithTokens = await Promise.all(collectionWithTokens); 22 | 23 | return resultCollectionWithTokens; 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/identity/ed25519/ed25519Identity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Ed25519KeyIdentity as baseEd25519Identity, Ed25519PublicKey 3 | } from '@dfinity/identity'; 4 | import { GenericSignIdentity } from '../genericSignIdentity'; 5 | import { 6 | blobToHex, 7 | BinaryBlob, 8 | } from '@dfinity/candid'; 9 | import { PublicKey } from '@dfinity/agent'; 10 | import { JsonnableKeyPair } from './../../../interfaces/identity' 11 | 12 | const PEM_BEGIN = `-----BEGIN EC PARAMETERS----- 13 | BgUrgQQACg== 14 | -----END EC PARAMETERS----- 15 | -----BEGIN EC PRIVATE KEY-----`; 16 | 17 | const PEM_END = '-----END EC PRIVATE KEY-----'; 18 | 19 | const PRIV_KEY_INIT = '3053020101300506032b657004220420'; 20 | 21 | const KEY_SEPARATOR = 'a123032100'; 22 | 23 | class Ed25519KeyIdentity extends baseEd25519Identity implements GenericSignIdentity { 24 | 25 | protected constructor( 26 | publicKey: PublicKey, 27 | protected _privateKey: BinaryBlob 28 | ) { 29 | super(publicKey, _privateKey); 30 | this._publicKey = Ed25519PublicKey.from(publicKey); 31 | } 32 | 33 | public getPem(): string { 34 | const rawPrivateKey = this._privateKey.toString('hex'); 35 | const rawPublicKey = this._publicKey.toRaw().toString('hex'); 36 | 37 | return `${PEM_BEGIN}\n${Buffer.from( 38 | `${PRIV_KEY_INIT}${rawPrivateKey}${KEY_SEPARATOR}${rawPublicKey}`, 39 | 'hex' 40 | ).toString('base64')}\n${PEM_END}`; 41 | } 42 | 43 | /** 44 | * Serialize this key to JSON. 45 | */ 46 | public toJSON(): JsonnableKeyPair { 47 | return [blobToHex(this._publicKey.toDer()), blobToHex(this._privateKey)]; 48 | } 49 | 50 | public static fromJSON(json: string): Ed25519KeyIdentity { 51 | const identity = super.fromJSON(json); 52 | const keyPair = identity.getKeyPair(); 53 | return new Ed25519KeyIdentity(keyPair.publicKey, keyPair.secretKey); 54 | } 55 | 56 | public static fromSecretKey(key: ArrayBuffer): Ed25519KeyIdentity { 57 | const identity = super.fromSecretKey(key); 58 | const keyPair = identity.getKeyPair(); 59 | return new Ed25519KeyIdentity(keyPair.publicKey, keyPair.secretKey); 60 | } 61 | 62 | } 63 | 64 | export default Ed25519KeyIdentity; 65 | -------------------------------------------------------------------------------- /src/utils/identity/genericSignIdentity.ts: -------------------------------------------------------------------------------- 1 | import { SignIdentity } from '@dfinity/agent'; 2 | import { JsonnableKeyPair } from './../../interfaces/identity' 3 | 4 | 5 | export abstract class GenericSignIdentity extends SignIdentity { 6 | /** 7 | * Serialize this key to JSON. 8 | */ 9 | abstract toJSON(): JsonnableKeyPair; 10 | 11 | /** 12 | * Return private key in a pem file 13 | */ 14 | abstract getPem(): string; 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/identity/identityFactory.ts: -------------------------------------------------------------------------------- 1 | import { GenericSignIdentity } from './genericSignIdentity'; 2 | import Secp256k1KeyIdentity from './secpk256k1/identity'; 3 | import Ed25519KeyIdentity from './ed25519/ed25519Identity' 4 | import { Types } from '../account/constants'; 5 | import { ERRORS } from '../../errors'; 6 | 7 | export class IdentityFactory { 8 | public static createIdentity(type: string, secretKey: string): GenericSignIdentity { 9 | switch (type) { 10 | case Types.secretKey256k1: 11 | case Types.pem256k1: 12 | case Types.mnemonic: 13 | return Secp256k1KeyIdentity.fromJSON(secretKey); 14 | case Types.pem25519: 15 | return Ed25519KeyIdentity.fromJSON(secretKey); 16 | default: 17 | throw new Error(ERRORS.INVALID_TYPE_ERROR); 18 | } 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/identity/parsePem.ts: -------------------------------------------------------------------------------- 1 | 2 | import Secp256k1KeyIdentity from './secpk256k1/identity'; 3 | import Ed25519KeyIdentity from './ed25519/ed25519Identity'; 4 | import { Types } from '../account/constants'; 5 | import { ERRORS, ERROR_CODES } from '../../errors'; 6 | 7 | const ED25519_KEY_INIT = '3053020101300506032b657004220420'; 8 | const ED25519_KEY_SEPARATOR = 'a123032100'; 9 | const ED25519_OID = '06032b6570'; 10 | 11 | const SEC256k1_KEY_INIT = '30740201010420'; 12 | const SEC256k1_KEY_SEPARATOR = 'a00706052b8104000aa144034200'; 13 | const SEC256k1_OID = '06052b8104000a' 14 | 15 | export const parseEd25519 = (pem: string) => { 16 | 17 | const raw = Buffer.from(pem, 'base64') 18 | .toString('hex') 19 | 20 | if (!raw.substring(0, 24).includes(ED25519_OID)) { 21 | return undefined; 22 | } 23 | 24 | const trimRaw = raw 25 | .replace(ED25519_KEY_INIT, '') 26 | .replace(ED25519_KEY_SEPARATOR, ''); 27 | 28 | try { 29 | const key = new Uint8Array(Buffer.from(trimRaw, 'hex')); 30 | const identity = Ed25519KeyIdentity.fromSecretKey(key); 31 | const type = Types.pem25519; 32 | return { identity, type }; 33 | } catch { 34 | return undefined 35 | } 36 | } 37 | 38 | export const parseSec256K1 = (pem: string) => { 39 | 40 | const raw = Buffer.from(pem, 'base64') 41 | .toString('hex') 42 | 43 | if (!raw.includes(SEC256k1_OID)) { 44 | return undefined; 45 | } 46 | 47 | const trimRaw = raw 48 | .replace(SEC256k1_KEY_INIT, '') 49 | .replace(SEC256k1_KEY_SEPARATOR, '') 50 | 51 | try { 52 | const key = new Uint8Array(Buffer.from(trimRaw.substring(0, 64), 'hex')); 53 | const identity = Secp256k1KeyIdentity.fromSecretKey(key); 54 | const type = Types.pem256k1; 55 | return { identity, type }; 56 | } catch { 57 | return undefined; 58 | } 59 | } 60 | 61 | export const getIdentityFromPem = (pem) => { 62 | 63 | const trimedPem = pem 64 | .replace(/(-{5}.*-{5})/g, '') 65 | .replace('\n', '') 66 | // Sepk256k1 keys 67 | .replace('BgUrgQQACg==', '') 68 | .trim(); 69 | 70 | const parsedIdentity = parseEd25519(trimedPem) || parseSec256K1(trimedPem); 71 | 72 | if (!parsedIdentity) throw new Error(ERROR_CODES.INVALID_KEY); 73 | 74 | return parsedIdentity; 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/utils/identity/secpk256k1/identity.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import Secp256k1 from 'secp256k1'; 3 | import { sha256 } from 'js-sha256'; 4 | 5 | import { 6 | blobFromHex, 7 | blobFromUint8Array, 8 | blobToHex, 9 | BinaryBlob, 10 | } from '@dfinity/candid'; 11 | import { PublicKey } from '@dfinity/agent'; 12 | import { GenericSignIdentity } from '../genericSignIdentity' 13 | import Secp256k1PublicKey from './publicKey'; 14 | import { JsonnableKeyPair } from './../../../interfaces/identity' 15 | 16 | const PEM_BEGIN = `-----BEGIN EC PARAMETERS----- 17 | BgUrgQQACg== 18 | -----END EC PARAMETERS----- 19 | -----BEGIN EC PRIVATE KEY-----`; 20 | 21 | const PEM_END = '-----END EC PRIVATE KEY-----'; 22 | 23 | const PRIV_KEY_INIT = '30740201010420'; 24 | 25 | const KEY_SEPARATOR = 'a00706052b8104000aa144034200'; 26 | 27 | class Secp256k1KeyIdentity extends GenericSignIdentity { 28 | public static fromParsedJson(obj: JsonnableKeyPair): Secp256k1KeyIdentity { 29 | const [publicKeyRaw, privateKeyRaw] = obj; 30 | return new Secp256k1KeyIdentity( 31 | Secp256k1PublicKey.fromRaw(blobFromHex(publicKeyRaw)), 32 | blobFromHex(privateKeyRaw) 33 | ); 34 | } 35 | 36 | public static fromJSON(json: string): Secp256k1KeyIdentity { 37 | const parsed = JSON.parse(json); 38 | if (Array.isArray(parsed)) { 39 | if (typeof parsed[0] === 'string' && typeof parsed[1] === 'string') { 40 | return this.fromParsedJson([parsed[0], parsed[1]]); 41 | } 42 | throw new Error( 43 | 'Deserialization error: JSON must have at least 2 items.' 44 | ); 45 | } else if (typeof parsed === 'object' && parsed !== null) { 46 | const { publicKey, _publicKey, secretKey, _privateKey } = parsed; 47 | const pk = publicKey 48 | ? Secp256k1PublicKey.fromRaw( 49 | blobFromUint8Array(new Uint8Array(publicKey.data)) 50 | ) 51 | : Secp256k1PublicKey.fromDer( 52 | blobFromUint8Array(new Uint8Array(_publicKey.data)) 53 | ); 54 | 55 | if (publicKey && secretKey && secretKey.data) { 56 | return new Secp256k1KeyIdentity( 57 | pk, 58 | blobFromUint8Array(new Uint8Array(secretKey.data)) 59 | ); 60 | } 61 | if (_publicKey && _privateKey && _privateKey.data) { 62 | return new Secp256k1KeyIdentity( 63 | pk, 64 | blobFromUint8Array(new Uint8Array(_privateKey.data)) 65 | ); 66 | } 67 | } 68 | throw new Error( 69 | `Deserialization error: Invalid JSON type for string: ${JSON.stringify( 70 | json 71 | )}` 72 | ); 73 | } 74 | 75 | public static fromKeyPair( 76 | publicKey: BinaryBlob, 77 | privateKey: BinaryBlob 78 | ): Secp256k1KeyIdentity { 79 | return new Secp256k1KeyIdentity( 80 | Secp256k1PublicKey.fromRaw(publicKey), 81 | privateKey 82 | ); 83 | } 84 | 85 | public static fromSecretKey(secretKey: ArrayBuffer): Secp256k1KeyIdentity { 86 | const publicKey = Secp256k1.publicKeyCreate( 87 | new Uint8Array(secretKey), 88 | false 89 | ); 90 | const identity = Secp256k1KeyIdentity.fromKeyPair( 91 | blobFromUint8Array(publicKey), 92 | blobFromUint8Array(new Uint8Array(secretKey)) 93 | ); 94 | return identity; 95 | } 96 | 97 | protected _publicKey: Secp256k1PublicKey; 98 | 99 | // `fromRaw` and `fromDer` should be used for instantiation, not this constructor. 100 | protected constructor( 101 | publicKey: PublicKey, 102 | protected _privateKey: BinaryBlob 103 | ) { 104 | super(); 105 | this._publicKey = Secp256k1PublicKey.from(publicKey); 106 | } 107 | 108 | /** 109 | * Serialize this key to JSON. 110 | */ 111 | public toJSON(): JsonnableKeyPair { 112 | return [blobToHex(this._publicKey.toRaw()), blobToHex(this._privateKey)]; 113 | } 114 | 115 | /** 116 | * Return a copy of the key pair. 117 | */ 118 | public getKeyPair(): { 119 | secretKey: BinaryBlob; 120 | publicKey: Secp256k1PublicKey; 121 | } { 122 | return { 123 | secretKey: blobFromUint8Array(new Uint8Array(this._privateKey)), 124 | publicKey: this._publicKey, 125 | }; 126 | } 127 | 128 | /** 129 | * Return the public key. 130 | */ 131 | public getPublicKey(): PublicKey { 132 | return this._publicKey; 133 | } 134 | 135 | /** 136 | * Return private key in a pem file 137 | */ 138 | 139 | public getPem(): string { 140 | const rawPrivateKey = this._privateKey.toString('hex'); 141 | const rawPublicKey = this._publicKey.toRaw().toString('hex'); 142 | 143 | return `${PEM_BEGIN}\n${Buffer.from( 144 | `${PRIV_KEY_INIT}${rawPrivateKey}${KEY_SEPARATOR}${rawPublicKey}`, 145 | 'hex' 146 | ).toString('base64')}\n${PEM_END}`; 147 | } 148 | 149 | /** 150 | * Signs a blob of data, with this identity's private key. 151 | * @param challenge - challenge to sign with this identity's secretKey, producing a signature 152 | */ 153 | public async sign(challenge: BinaryBlob): Promise { 154 | const hash = sha256.create(); 155 | hash.update(challenge); 156 | const { signature } = Secp256k1.ecdsaSign( 157 | new Uint8Array(hash.digest()), 158 | this._privateKey 159 | ); 160 | return blobFromUint8Array(signature); 161 | } 162 | } 163 | 164 | export default Secp256k1KeyIdentity; 165 | -------------------------------------------------------------------------------- /src/utils/identity/secpk256k1/publicKey.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@dfinity/agent'; 2 | import { 3 | BinaryBlob, 4 | blobFromUint8Array, 5 | derBlobFromBlob, 6 | DerEncodedBlob, 7 | } from '@dfinity/candid'; 8 | 9 | // This implementation is adjusted from the Ed25519PublicKey. 10 | // The RAW_KEY_LENGTH and DER_PREFIX are modified accordingly 11 | class Secp256k1PublicKey implements PublicKey { 12 | public static fromRaw(rawKey: BinaryBlob): Secp256k1PublicKey { 13 | return new Secp256k1PublicKey(rawKey); 14 | } 15 | 16 | public static fromDer( 17 | derKey: BinaryBlob | DerEncodedBlob 18 | ): Secp256k1PublicKey { 19 | return new Secp256k1PublicKey(this.derDecode(derKey as BinaryBlob)); 20 | } 21 | 22 | public static from(key: PublicKey): Secp256k1PublicKey { 23 | return this.fromDer(key.toDer()); 24 | } 25 | 26 | // The length of secp256k1 public keys is always 65 bytes. 27 | private static RAW_KEY_LENGTH = 65; 28 | 29 | // Adding this prefix to a raw public key is sufficient to DER-encode it. 30 | // prettier-ignore 31 | private static DER_PREFIX = Uint8Array.from([ 32 | 0x30, 0x56, // SEQUENCE 33 | 0x30, 0x10, // SEQUENCE 34 | 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // OID ECDSA 35 | 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a, // OID secp256k1 36 | 0x03, 0x42, // BIT STRING 37 | 0x00, // no padding 38 | ]); 39 | 40 | private static derEncode(publicKey: BinaryBlob): DerEncodedBlob { 41 | if (publicKey.byteLength !== Secp256k1PublicKey.RAW_KEY_LENGTH) { 42 | const bl = publicKey.byteLength; 43 | throw new TypeError( 44 | `secp256k1 public key must be ${Secp256k1PublicKey.RAW_KEY_LENGTH} bytes long (is ${bl})` 45 | ); 46 | } 47 | 48 | const derPublicKey = Uint8Array.from([ 49 | ...Secp256k1PublicKey.DER_PREFIX, 50 | ...new Uint8Array(publicKey), 51 | ]); 52 | 53 | return derBlobFromBlob(blobFromUint8Array(derPublicKey)); 54 | } 55 | 56 | private static derDecode(key: BinaryBlob): BinaryBlob { 57 | const expectedLength = 58 | Secp256k1PublicKey.DER_PREFIX.length + Secp256k1PublicKey.RAW_KEY_LENGTH; 59 | if (key.byteLength !== expectedLength) { 60 | const bl = key.byteLength; 61 | throw new TypeError( 62 | `secp256k1 DER-encoded public key must be ${expectedLength} bytes long (is ${bl})` 63 | ); 64 | } 65 | 66 | const rawKey = blobFromUint8Array( 67 | key.subarray(Secp256k1PublicKey.DER_PREFIX.length) 68 | ); 69 | if (!this.derEncode(rawKey).equals(key)) { 70 | throw new TypeError( 71 | 'secp256k1 DER-encoded public key is invalid. A valid secp256k1 DER-encoded public key ' + 72 | `must have the following prefix: ${Secp256k1PublicKey.DER_PREFIX}` 73 | ); 74 | } 75 | 76 | return rawKey; 77 | } 78 | 79 | private readonly rawKey: BinaryBlob; 80 | 81 | private readonly derKey: DerEncodedBlob; 82 | 83 | // `fromRaw` and `fromDer` should be used for instantiation, not this constructor. 84 | private constructor(key: BinaryBlob) { 85 | this.rawKey = key; 86 | this.derKey = Secp256k1PublicKey.derEncode(key); 87 | } 88 | 89 | public toDer(): DerEncodedBlob { 90 | return this.derKey; 91 | } 92 | 93 | public toRaw(): BinaryBlob { 94 | return this.rawKey; 95 | } 96 | } 97 | export default Secp256k1PublicKey; 98 | -------------------------------------------------------------------------------- /src/utils/object.ts: -------------------------------------------------------------------------------- 1 | import { Principal } from '@dfinity/principal'; 2 | import JsonBigInt from 'json-bigint'; 3 | 4 | // eslint-disable-next-line 5 | export const recursiveParseBigint = obj => JsonBigInt.parse(JsonBigInt.stringify(obj)); 6 | 7 | export const parsePrincipal = pidObj => 8 | pidObj?._isPrincipal 9 | ? Principal.fromUint8Array( 10 | new Uint8Array(Object.values((pidObj as any)._arr)) 11 | ).toString() 12 | : pidObj; 13 | -------------------------------------------------------------------------------- /src/utils/signature/index.ts: -------------------------------------------------------------------------------- 1 | import { BinaryBlob } from '@dfinity/candid'; 2 | import { TextEncoder } from 'text-encoding-shim'; 3 | import Secp256k1 from 'secp256k1'; 4 | import Secp256k1PublicKey from '../identity/secpk256k1/publicKey'; 5 | 6 | const encoder = new TextEncoder('utf-8'); 7 | 8 | export const sign = (message: string, secretKey: BinaryBlob): Uint8Array => { 9 | const encoded = encoder.encode(message); 10 | const buffer = Buffer.from([ 11 | ...Buffer.alloc(32 - encoded.length).fill(0), 12 | ...encoded, 13 | ]); 14 | const { signature } = Secp256k1.ecdsaSign(buffer, secretKey); 15 | return signature; 16 | }; 17 | export const verify = ( 18 | message: string, 19 | signature: Uint8Array, 20 | publicKey: Secp256k1PublicKey 21 | ): boolean => { 22 | const encoded = encoder.encode(message); 23 | const buffer = Buffer.from([ 24 | ...Buffer.alloc(32 - encoded.length).fill(0), 25 | ...encoded, 26 | ]); 27 | return Secp256k1.ecdsaVerify(signature, buffer, publicKey.toRaw()); 28 | }; 29 | -------------------------------------------------------------------------------- /src/utils/storage/index.ts: -------------------------------------------------------------------------------- 1 | import extension from 'extensionizer'; 2 | import { isEmpty, checkForError } from './utils'; 3 | 4 | /* eslint-disable no-underscore-dangle */ 5 | /* eslint-disable class-methods-use-this */ 6 | 7 | /** 8 | * A wrapper around the extension's storage local API 9 | */ 10 | class ExtensionStore { 11 | isSupported: boolean; 12 | 13 | constructor() { 14 | this.isSupported = Boolean(extension?.storage?.local); 15 | if (!this.isSupported) { 16 | console.log('Storage not supported in this browser'); // eslint-disable-line 17 | } 18 | } 19 | 20 | /** 21 | * Returns all of the keys currently saved 22 | * @returns {Promise<*>} 23 | */ 24 | public async get(): Promise { 25 | if (!this.isSupported) { 26 | return undefined; 27 | } 28 | const result = await this._get(); 29 | // extension?.storage.local always returns an obj 30 | // if the object is empty, treat it as undefined 31 | if (isEmpty(result)) { 32 | return undefined; 33 | } 34 | return result; 35 | } 36 | 37 | /** 38 | * Sets the key in local state 39 | * @param {Object} state - The state to set 40 | * @returns {Promise} 41 | */ 42 | public async set(state): Promise { 43 | return this._set(state); 44 | } 45 | 46 | /** 47 | * Returns all of the keys currently saved 48 | * @private 49 | * @returns {Object} the key-value map from local storage 50 | */ 51 | private _get(): Promise { 52 | const { local } = extension?.storage; 53 | return new Promise((resolve, reject) => { 54 | local.get(null, (/** @type {any} */ result) => { 55 | const err = checkForError(); 56 | if (err) { 57 | reject(err); 58 | } else { 59 | resolve(result); 60 | } 61 | }); 62 | }); 63 | } 64 | 65 | /** 66 | * Sets the key in local state 67 | * @param {Object} obj - The key to set 68 | * @returns {Promise} 69 | * @private 70 | */ 71 | private _set(obj): Promise { 72 | const { local } = extension?.storage; 73 | return new Promise((resolve, reject) => { 74 | local.set(obj, () => { 75 | const err = checkForError(); 76 | if (err) { 77 | reject(err); 78 | } else { 79 | resolve(); 80 | } 81 | }); 82 | }); 83 | } 84 | 85 | public async clear(): Promise { 86 | const { local } = extension?.storage; 87 | return new Promise((resolve, reject) => { 88 | local.clear(() => { 89 | const err = checkForError(); 90 | if (err) { 91 | reject(err); 92 | } else { 93 | resolve(); 94 | } 95 | }); 96 | }); 97 | } 98 | } 99 | 100 | export default ExtensionStore; 101 | -------------------------------------------------------------------------------- /src/utils/storage/mock.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | class StorageMock { 3 | private store: any; 4 | 5 | public isSupported: boolean; 6 | 7 | public local: { 8 | get: () => any; 9 | set: (obj: any) => void; 10 | }; 11 | 12 | public constructor() { 13 | this.store = {}; 14 | this.local = { 15 | set: this.set.bind(this), 16 | get: this.get.bind(this), 17 | }; 18 | this.isSupported = true; 19 | } 20 | 21 | public set = (obj = {}): any => { 22 | this.store = { 23 | ...this.store, 24 | ...obj, 25 | }; 26 | }; 27 | 28 | public get = (): any => { 29 | return { ...this.store }; 30 | }; 31 | 32 | public clear = (): any => { 33 | this.set({}); 34 | return {}; 35 | }; 36 | } 37 | 38 | const store = new StorageMock(); 39 | 40 | export default store; 41 | -------------------------------------------------------------------------------- /src/utils/storage/update_handlers/v0.14.5.ts: -------------------------------------------------------------------------------- 1 | import { TOKENS } from '../../../constants/tokens'; 2 | import { PlugStateStorage } from '../../../interfaces/plug_keyring'; 3 | 4 | export default (storage: any): PlugStateStorage => ({ 5 | ...storage, 6 | wallets: storage.wallets.map(wallet => ({ 7 | ...wallet, 8 | assets: wallet.assets.reduce( 9 | (acum, asset) => ({ 10 | ...acum, 11 | [asset.canisterId]: { 12 | amount: '0', 13 | token: { 14 | name: asset.name, 15 | symbol: asset.symbol, 16 | canisterId: asset.canisterId, 17 | standard: 18 | wallet.registeredTokens[ 19 | asset.canisterId 20 | ]?.standard.toUpperCase() || TOKENS[asset.symbol].standard, 21 | decimals: 22 | wallet.registeredTokens[asset.canisterId]?.decimals || 23 | TOKENS[asset.symbol].decimals, 24 | color: wallet.registeredTokens[asset.canisterId]?.color, 25 | }, 26 | }, 27 | }), 28 | {} 29 | ), 30 | })), 31 | }); 32 | -------------------------------------------------------------------------------- /src/utils/storage/update_handlers/v0.16.8.ts: -------------------------------------------------------------------------------- 1 | import { LEDGER_CANISTER_ID } from '../../dfx/constants'; 2 | import { TOKENS } from '../../../constants/tokens'; 3 | 4 | export default (storage: any) => { 5 | return { 6 | ...storage, wallets: storage.wallets.map( 7 | (wallet) => { 8 | delete wallet.assets.null; 9 | return ({ 10 | ...wallet, 11 | assets: { [LEDGER_CANISTER_ID]: { amount: '0', token: TOKENS.ICP }, ...wallet.assets }, 12 | }) 13 | }) 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/storage/update_handlers/v0.19.3.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_MAINNET_TOKENS } from '../../../constants/tokens'; 2 | import { Assets, JSONWallet } from '../../../interfaces/plug_wallet'; 3 | import { Mainnet } from '../../../PlugKeyRing/modules/NetworkModule/Network'; 4 | 5 | type JSONWalletLegacy = JSONWallet & { walletNumber: number, assets: Assets } 6 | 7 | export default (storage: any) => { 8 | const defaultTokenIds = DEFAULT_MAINNET_TOKENS.map((t) => t.canisterId); 9 | const wallets: JSONWalletLegacy[] = storage.wallets; 10 | 11 | // For each wallet, get its tokens and add the wallet id to the list of registered by 12 | const nestedTokens = wallets?.map((wallet) => (Object.values(wallet?.assets) || []).map( 13 | (asset) => ({ 14 | ...(asset?.token || {}), 15 | registeredBy: [wallet.walletNumber], 16 | logo: asset?.token?.logo || (asset?.token as any)?.image, 17 | }), 18 | )); 19 | const tokens = nestedTokens?.flat?.() || []; 20 | // Remove default tokens and merge duplicates, concatting their registeredBy 21 | const registeredTokens = tokens.reduce((acum, token) => { 22 | 23 | // Remove default tokens 24 | if (defaultTokenIds.includes(token.canisterId)) { 25 | return acum; 26 | } 27 | 28 | // If we already had this token, only update the registeredby array 29 | const registeredTokenIndex = acum.findIndex((t) => t.canisterId === token.canisterId); 30 | if (registeredTokenIndex > -1) { 31 | const registeredToken = acum[registeredTokenIndex]; 32 | const updatedToken = { 33 | ...registeredToken, 34 | registeredBy: [...registeredToken.registeredBy, ...token.registeredBy], 35 | }; 36 | const copy = [...acum]; 37 | copy[registeredTokenIndex] = updatedToken; 38 | return copy; 39 | } 40 | // Else just add the token (first time we find it) 41 | return [...acum, token]; 42 | }, [] as any); 43 | 44 | const newStorage = { 45 | ...storage, 46 | networkModule: { 47 | ...(storage.networkModule || {}), 48 | networks: { 49 | ...(storage?.networkModule?.networks || {}), 50 | mainnet: (new Mainnet({ registeredTokens }, fetch)).toJSON() 51 | }, 52 | networkId: storage?.networkModule?.networkId || 'mainnet', 53 | }, 54 | }; 55 | return newStorage; 56 | }; 57 | -------------------------------------------------------------------------------- /src/utils/storage/update_handlers/v0.20.0.ts: -------------------------------------------------------------------------------- 1 | import { createAccountFromMnemonic } from '../../../utils/account'; 2 | import { Types } from '../../../utils/account/constants'; 3 | import Secp256k1KeyIdentity from '../../identity/secpk256k1/identity'; 4 | 5 | const getKeyPair = (identity: Secp256k1KeyIdentity) => { 6 | const jsonIdentity = identity.toJSON(); 7 | return typeof jsonIdentity === 'string' 8 | ? jsonIdentity 9 | : JSON.stringify(jsonIdentity); 10 | } 11 | 12 | export default (storage: any) => { 13 | const mnemonic = storage.mnemonic; 14 | return { 15 | ...storage, wallets: storage.wallets.map( 16 | (wallet) => { 17 | const { identity } = createAccountFromMnemonic( 18 | mnemonic, 19 | wallet.walletNumber 20 | ); 21 | return ({ 22 | ...wallet, 23 | type: Types.mnemonic, 24 | keyPair: getKeyPair(identity), 25 | }) 26 | }) 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/utils/storage/update_handlers/v0.21.0.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from "uuid"; 2 | import { Network } from '../../../PlugKeyRing/modules/NetworkModule/Network' 3 | 4 | export default (storage: any) => { 5 | const walletIds = storage.wallets.map(()=> uuid()) 6 | const networkModule = storage.networkModuleBis || storage.networkModule; 7 | const networks: { [networkId: string]: Network } = networkModule && networkModule.networks; 8 | 9 | const editedNetworks = ((networks && Object.values(networks)) || []).map((network) => ({ 10 | ...network, 11 | registeredTokens: network.registeredTokens.map((token)=> { 12 | const registeredBy = token.registeredBy.map((walletNumber) => walletIds[walletNumber]); 13 | return ({ 14 | ...token, 15 | registeredBy, 16 | }) 17 | }) 18 | })) 19 | 20 | const finalNetwork = editedNetworks.reduce((accum, network)=> ({ 21 | ...accum, 22 | [network.id]: network, 23 | }), {}) 24 | 25 | return { 26 | ...storage, 27 | wallets: storage.wallets.reduce( 28 | (walletsAccum, wallet, index) => { 29 | const walletId = walletIds[index]; 30 | return ({ 31 | ...walletsAccum, 32 | [walletId]: { ...wallet, walletId, orderNumber: index } 33 | }) 34 | }, {}), 35 | walletIds, 36 | mnemonicWalletCount: walletIds.length, 37 | currentWalletId: walletIds[0], 38 | ...(networks && { networkModule: { 39 | ...storage.networkModule, 40 | networks: finalNetwork 41 | } } ), 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /src/utils/storage/utils.ts: -------------------------------------------------------------------------------- 1 | import extension from 'extensionizer'; 2 | 3 | import handler14_5 from './update_handlers/v0.14.5'; 4 | import handler16_8 from './update_handlers/v0.16.8'; 5 | import handler19_3 from './update_handlers/v0.19.3'; 6 | import handler20_0 from './update_handlers/v0.20.0'; 7 | import handler21_0 from './update_handlers/v0.21.0'; 8 | 9 | import { PlugStateStorage } from '../../interfaces/plug_keyring'; 10 | import { NetworkModuleParams } from '../../PlugKeyRing/modules/NetworkModule'; 11 | 12 | export const isEmpty = (obj): boolean => Object.keys(obj).length === 0; 13 | 14 | /** 15 | * Returns an Error if extension.runtime.lastError is present 16 | * this is a workaround for the non-standard error object that's used 17 | * @returns {Error|undefined} 18 | */ 19 | export const checkForError = (): Error | undefined => { 20 | const { lastError } = extension.runtime; 21 | if (!lastError) { 22 | return undefined; 23 | } 24 | // if it quacks like an Error, its an Error 25 | if (lastError.stack && lastError.message) { 26 | return lastError; 27 | } 28 | // repair incomplete error object (eg chromium v77) 29 | return new Error(lastError.message); 30 | }; 31 | 32 | const VERSION_PATH: Array = [ 33 | '0.14.1', 34 | '0.14.5', 35 | '0.16.8', 36 | '0.19.3', 37 | '0.20.0', 38 | '0.21.0', 39 | ]; 40 | 41 | const VERSION_HANDLER: { 42 | [version: string]: (storage: any) => PlugStateStorage; 43 | } = { 44 | '0.14.1': (storage: any) => { 45 | return storage; 46 | }, 47 | '0.14.5': handler14_5, 48 | '0.16.8': handler16_8, 49 | '0.19.3': handler19_3, 50 | '0.20.0': handler20_0, 51 | '0.21.0': handler21_0, 52 | }; 53 | 54 | const compareVersion = (a: string, b: string): number => { 55 | const arrA = a.split('.'); 56 | const arrB = b.split('.'); 57 | if (arrA.length !== 3 || arrB.length !== 3) 58 | throw Error('Storage Hande Update: invalid version'); 59 | for (let index = 0; index < 3; index++) { 60 | const numbA = parseInt(arrA[index]); 61 | const numbB = parseInt(arrB[index]); 62 | if (numbA > numbB) return -1; 63 | else if (numbA < numbB) return 1; 64 | } 65 | return 0; 66 | }; 67 | 68 | const getVersionIndex = (version: string | undefined): number => { 69 | if (!version) return 0; 70 | 71 | for (let index = 0; index < VERSION_PATH.length; index++) { 72 | const comparison = compareVersion(version, VERSION_PATH[index]); 73 | if (comparison === 1) return index; 74 | if (comparison === 0) return index + 1; 75 | } 76 | 77 | return VERSION_PATH.length; 78 | }; 79 | 80 | export const handleStorageUpdate = ( 81 | storageVersion: string | undefined, 82 | storage: any 83 | ): PlugStateStorage & { 84 | mnemonic: string; 85 | networkModule?: NetworkModuleParams; 86 | } => { 87 | const index = getVersionIndex(storageVersion); 88 | if (index === VERSION_PATH.length) return storage; 89 | 90 | let newStorage = storage; 91 | VERSION_PATH.slice(index).forEach(version => { 92 | console.log(`APPLYING STORAGE UPDATE V${version}...`); 93 | newStorage = VERSION_HANDLER[version](newStorage); 94 | }); 95 | return newStorage; 96 | }; 97 | -------------------------------------------------------------------------------- /src/utils/version.ts: -------------------------------------------------------------------------------- 1 | import { PLUG_CONTROLLER_VERSION } from '../constants/version' 2 | 3 | export const getVersion = () => { 4 | return PLUG_CONTROLLER_VERSION; 5 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "rootDir": "./src", 5 | "declaration": true, 6 | "noImplicitAny": false, 7 | "esModuleInterop": true, 8 | "strictNullChecks": true, 9 | "module": "commonjs", 10 | "target": "es2016", 11 | "lib": [ 12 | "es2020" 13 | ], 14 | "resolveJsonModule": true, 15 | "experimentalDecorators": true, 16 | "emitDecoratorMetadata": true, 17 | "noUnusedParameters": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "noImplicitReturns": true, 20 | "skipLibCheck": true, 21 | "types": ["node", "jest"] 22 | }, 23 | "compileOnSave": true, 24 | "exclude": [ 25 | "node_modules", 26 | "dist", 27 | "tests", 28 | "jest.config.cjs", 29 | ".vscode" 30 | ] 31 | } 32 | --------------------------------------------------------------------------------