├── .eslintrc ├── .github ├── dependabot.yml └── workflows │ ├── pr.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── pre-push ├── .prettierrc ├── .releaserc ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── src ├── common.ts ├── index.ts └── types.ts ├── test └── index.test.ts ├── tsconfig.eslint.json ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2022": true, 5 | "webextensions": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:prettier/recommended" 13 | ], 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "ecmaVersion": 2022, 17 | "sourceType": "module" 18 | }, 19 | "plugins": [ 20 | "@typescript-eslint" 21 | ] 22 | } 23 | 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "yarn" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "sunday" 8 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | jobs: 7 | build: 8 | name: Build 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Setup Build Node 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: "16" 16 | - run: yarn install --frozen-lockfile 17 | - run: yarn test 18 | - run: yarn build 19 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | release: 8 | name: Release 9 | permissions: 10 | contents: write 11 | issues: write 12 | pull-requests: write 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Setup Node 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: "16" 20 | - run: yarn install --frozen-lockfile 21 | - run: yarn test 22 | - run: yarn build 23 | - env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | NPM_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} 26 | run: npx semantic-release 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### VisualStudioCode template 2 | .vscode/* 3 | !.vscode/settings.json 4 | !.vscode/tasks.json 5 | !.vscode/launch.json 6 | !.vscode/extensions.json 7 | !.vscode/*.code-snippets 8 | 9 | # Local History for Visual Studio Code 10 | .history/ 11 | 12 | # Built Visual Studio Code Extensions 13 | *.vsix 14 | 15 | ### JetBrains template 16 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 17 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 18 | 19 | # User-specific stuff 20 | .idea/ 21 | 22 | # CMake 23 | cmake-build-*/ 24 | 25 | # File-based project format 26 | *.iws 27 | 28 | # IntelliJ 29 | out/ 30 | 31 | # mpeltonen/sbt-idea plugin 32 | .idea_modules/ 33 | 34 | # JIRA plugin 35 | atlassian-ide-plugin.xml 36 | 37 | 38 | # Crashlytics plugin (for Android Studio and IntelliJ) 39 | com_crashlytics_export_strings.xml 40 | crashlytics.properties 41 | crashlytics-build.properties 42 | fabric.properties 43 | 44 | 45 | ### Node template 46 | # Logs 47 | logs 48 | *.log 49 | npm-debug.log* 50 | yarn-debug.log* 51 | yarn-error.log* 52 | lerna-debug.log* 53 | .pnpm-debug.log* 54 | 55 | # Diagnostic reports (https://nodejs.org/api/report.html) 56 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 57 | 58 | # Runtime data 59 | pids 60 | *.pid 61 | *.seed 62 | *.pid.lock 63 | 64 | # Directory for instrumented libs generated by jscoverage/JSCover 65 | lib-cov 66 | 67 | # Coverage directory used by tools like istanbul 68 | coverage 69 | *.lcov 70 | 71 | # nyc test coverage 72 | .nyc_output 73 | 74 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 75 | .grunt 76 | 77 | # Bower dependency directory (https://bower.io/) 78 | bower_components 79 | 80 | # node-waf configuration 81 | .lock-wscript 82 | 83 | # Compiled binary addons (https://nodejs.org/api/addons.html) 84 | build/Release 85 | 86 | # Dependency directories 87 | node_modules/ 88 | jspm_packages/ 89 | 90 | # Snowpack dependency directory (https://snowpack.dev/) 91 | web_modules/ 92 | 93 | # TypeScript cache 94 | *.tsbuildinfo 95 | 96 | # Optional npm cache directory 97 | .npm 98 | 99 | # Optional eslint cache 100 | .eslintcache 101 | 102 | # Optional stylelint cache 103 | .stylelintcache 104 | 105 | # Microbundle cache 106 | .rpt2_cache/ 107 | .rts2_cache_cjs/ 108 | .rts2_cache_es/ 109 | .rts2_cache_umd/ 110 | 111 | # Optional REPL history 112 | .node_repl_history 113 | 114 | # Output of 'npm pack' 115 | *.tgz 116 | 117 | # Yarn Integrity file 118 | .yarn-integrity 119 | 120 | # dotenv environment variable files 121 | .env 122 | .env.development.local 123 | .env.test.local 124 | .env.production.local 125 | .env.local 126 | 127 | # parcel-bundler cache (https://parceljs.org/) 128 | .cache 129 | .parcel-cache 130 | 131 | # Next.js build output 132 | .next 133 | out 134 | 135 | # Nuxt.js build / generate output 136 | .nuxt 137 | dist 138 | 139 | # Gatsby files 140 | .cache/ 141 | # Comment in the public line in if your project uses Gatsby and not Next.js 142 | # https://nextjs.org/blog/next-9-1#public-directory-support 143 | # public 144 | 145 | # vuepress build output 146 | .vuepress/dist 147 | 148 | # vuepress v2.x temp and cache directory 149 | .temp 150 | 151 | # Docusaurus cache and generated files 152 | .docusaurus 153 | 154 | # Serverless directories 155 | .serverless/ 156 | 157 | # FuseBox cache 158 | .fusebox/ 159 | 160 | # DynamoDB Local files 161 | .dynamodb/ 162 | 163 | # TernJS port file 164 | .tern-port 165 | 166 | # Stores VSCode versions used for testing VSCode extensions 167 | .vscode-test 168 | 169 | # yarn v2 170 | .yarn/cache 171 | .yarn/unplugged 172 | .yarn/build-state.yml 173 | .yarn/install-state.gz 174 | .pnp.* 175 | 176 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged || exit 1 5 | 6 | # Fail on direct commits to master 7 | branch="$(git rev-parse --abbrev-ref HEAD)" 8 | if [ "$branch" = "master" ]; then 9 | echo "No committing directly to master" 10 | exit 1 11 | fi 12 | 13 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | eslint ./src/ 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false, 4 | "bracketSpacing": true, 5 | "trailingComma": "es5", 6 | "printWidth": 80, 7 | "singleQuote": false, 8 | "bracketSameLine": true, 9 | "semi": true, 10 | "extensions": [".ts", ".js", ".json"], 11 | "arrowParens": "avoid", 12 | "proseWrap": "always" 13 | } 14 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "master" 4 | ], 5 | "plugins": [ 6 | "@semantic-release/commit-analyzer", 7 | "@semantic-release/release-notes-generator", 8 | "@semantic-release/changelog", 9 | "@semantic-release/npm", 10 | [ 11 | "@semantic-release/git", 12 | { 13 | "assets": [ 14 | "package.json", 15 | "yarn.lock", 16 | "CHANGELOG.md" 17 | ], 18 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 19 | } 20 | ], 21 | "@semantic-release/github" 22 | ], 23 | "repositoryUrl": "https://github.com/myBraavos/starknet-url" 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 avimak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # starknet-url 2 | 3 | StarkNet URL generator & parser. 4 | \ 5 | Supporting common Ethereum URL standards such as [ERC-681](https://eips.ethereum.org/EIPS/eip-681) and [ERC-831](https://eips.ethereum.org/EIPS/eip-831). 6 | \ 7 | \ 8 | This is a base standardizing lib to be used mainly by infrastructure devs/builders.\ 9 | Use [starknet-deeplink](https://www.npmjs.com/package/starknet-deeplink) 10 | for easily generating StarkNet deeplinks - such as payment requests, dApp launching, and more. 11 | ## Installation 12 | 13 | using npm - 14 | ```bash 15 | npm install starknet-url 16 | ``` 17 | or yarn - 18 | 19 | ```bash 20 | yarn add starknet-url 21 | ``` 22 | 23 | 24 | ## Examples 25 | 26 | Goerli ETH payment request: 27 | 28 | ```javascript 29 | import { STARKNET_ETH, StarknetChainId, transfer } from 'starknet-url' 30 | 31 | const beneficiaryAddress = "0x123456789abcdef"; 32 | const url = transfer(beneficiaryAddress, { 33 | token: { 34 | token_address: STARKNET_ETH, 35 | chainId: StarknetChainId.GOERLI 36 | }, 37 | amount: 0.02 38 | }) 39 | 40 | // "starknet:0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7@0x534e5f474f45524c49/transfer?address=0x123456789abcdef&uint256=2e-2" 41 | ``` 42 | \ 43 | Add (listing) token request: 44 | 45 | ```javascript 46 | import { addToken, StarknetChainId } from 'starknet-url' 47 | 48 | 49 | const url = addToken({ 50 | token_address: "0x123456789abcdef", 51 | chainId: StarknetChainId.GOERLI, 52 | }); 53 | 54 | // "starknet:0x123456789abcdef@0x534e5f474f45524c49/watchAsset?type=ERC20" 55 | ``` 56 | \ 57 | dApp launching request: 58 | 59 | ```javascript 60 | import { dapp } from 'starknet-url' 61 | 62 | 63 | const url = dapp("https://example.com"); 64 | 65 | // "starknet:dapp-example.com" 66 | ``` 67 | \ 68 | Custom URI: 69 | 70 | ```javascript 71 | import { build, parse } from 'starknet-url' 72 | 73 | 74 | // build a custom URI 75 | const url = build({ 76 | target_address: "0x12456789abcdef", 77 | function_name: "custom", 78 | chain_id: "0x13579", 79 | parameters: { 80 | "my": "custom", 81 | "function": "params", 82 | }, 83 | }) 84 | // "starknet:0x12456789abcdef@0x13579/custom?my=custom&function=params" 85 | 86 | // parse a custom URI 87 | const parsed = parse(url); 88 | /* 89 | { 90 | schema: "starknet", 91 | target_address: "0x12456789abcdef", 92 | chain_id: "0x13579", 93 | function_name: "custom", 94 | parameters: { my: "custom", function: "params" } 95 | } 96 | */ 97 | ``` 98 | ## API Reference 99 | 100 | ### build _(options: BuildOptions)_ 101 | Build a StarkNet URI. 102 | \ 103 | returns a URI `string` 104 | 105 | | Parameter | Type | Description | 106 | |:----------|:---------------|:-----------------------------------------| 107 | | `options` | `BuildOptions` | **Required**. StarkNet URI build options | 108 | 109 | 110 | ### parse _(uri: string)_ 111 | Parse a StarkNet URI. 112 | \ 113 | returns a `ParseResult` object 114 | 115 | | Parameter | Type | Description | 116 | |:----------|:---------|:---------------------------| 117 | | `uri` | `string` | **Required**. StarkNet URI | 118 | 119 | ### transfer _(to_address: string, options: TransferOptions)_ 120 | Generate a `transfer` StarkNet URI, i.e. for a payment request. 121 | \ 122 | returns a URI `string` 123 | 124 | | Parameter | Type | Description | 125 | |:-------------|:------------------|:--------------------------------------------------------------------------------------| 126 | | `to_address` | `string` | **Required**. beneficiary address | 127 | | `options` | `TransferOptions` | **Required**. defines a token to be used, and optionally the amount to be transferred | 128 | 129 | ### addToken _(token: Token)_ 130 | Generate a `watchAsset` StarkNet URI, for watching the given token (asset). 131 | \ 132 | returns a URI `string` 133 | 134 | | Parameter | Type | Description | 135 | |:----------|:--------|:--------------------------------------------------------------------------| 136 | | `token` | `Token` | **Required**. the token to be added (listed) by this `watchAsset` request | 137 | 138 | ### dapp _(url: string)_ 139 | Generate a `dapp` StarkNet URI, for launching a dApp in a StarkNet supporting browser client. 140 | \ 141 | returns a URI `string` 142 | 143 | | Parameter | Type | Description | 144 | |:----------|:---------|:-----------------------| 145 | | `url` | `string` | **Required**. dapp url | 146 | 147 | ### Types 148 | 149 | #### BuildOptions 150 | 151 | | Parameter | Type | Description | 152 | |:-----------------|:--------------------|:---------------------------------------------------------------------------------------------------------------------------------------| 153 | | `target_address` | `string` | **Required**. defines either the beneficiary of native token payment, or the contract address with which the user is asked to interact | 154 | | `prefix` | `string` | **Optional**. defines the use-case for this URI | 155 | | `chain_id` | `string` | **Optional**. defines the decimal chain ID, such that transactions on various test- and private networks can be requested | 156 | | `function_name` | `string` | **Optional**. `transfer`, `dapp`, `watchAsset`, etc. | 157 | | `parameters` | `{ [key]: string }` | **Optional**. defines extra parameters per use-case | 158 | 159 | #### ParseResult 160 | 161 | Same as *BuildOptions* (per given URI use-case), plus `schema` which must always be `starknet:` 162 | 163 | #### TransferOptions 164 | | Parameter | Type | Description | 165 | |:----------|:---------------------|:-------------------------------| 166 | | `token` | `Token` | **Required**. token to be used | 167 | | `amount` | `string` or `number` | **Optional**. requested amount | 168 | 169 | 170 | #### Token 171 | | Parameter | Type | Description | 172 | |:----------------|:----------|:---------------------------------------| 173 | | `token_address` | `string` | **Required**. the token address | 174 | | `chainId` | `ChainId` | **Required**. token address's chain ID | 175 | 176 | #### ChainId 177 | Could be either `StarknetChainId`, `string` or a `number` (hex) 178 | 179 | #### StarknetChainId 180 | `enum` of common StarkNet chain IDs, such as - 181 | ``` 182 | MAINNET = "0x534e5f4d41494e", 183 | GOERLI = "0x534e5f474f45524c49", 184 | GOERLI2 = "0x534e5f474f45524c4932", 185 | ``` 186 | ## Acknowledgements 187 | This lib is inspired by the excellent Ethereum (L1) build/parse lib: [eth-url-parser](https://github.com/brunobar79/eth-url-parser). 188 | 189 | ## License 190 | 191 | [MIT](https://choosealicense.com/licenses/mit/) 192 | 193 | 194 | ## Related 195 | 196 | [starknet-deeplink](https://www.npmjs.com/package/starknet-deeplink) - StarkNet deeplink generator 197 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | export default { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starknet-url", 3 | "version": "1.0.2", 4 | "description": "Build & parse StarkNet URLs", 5 | "repository": { 6 | "url": "https://github.com/myBraavos/starknet-url" 7 | }, 8 | "keywords": [ 9 | "erc681", 10 | "erc831", 11 | "starknet", 12 | "starkware", 13 | "l2", 14 | "zk", 15 | "rollup", 16 | "dapp", 17 | "url", 18 | "parser", 19 | "deeplink", 20 | "web3" 21 | ], 22 | "main": "dist/index.js", 23 | "types": "dist/index.d.ts", 24 | "files": [ 25 | "dist" 26 | ], 27 | "type": "module", 28 | "license": "MIT", 29 | "scripts": { 30 | "test": "jest", 31 | "build": "WEBPACK_MODE=production webpack", 32 | "dev": "WEBPACK_MODE=development webpack -w", 33 | "prepare": "yarn build && husky install" 34 | }, 35 | "devDependencies": { 36 | "@commitlint/cli": "^17.3.0", 37 | "@commitlint/config-conventional": "^17.3.0", 38 | "@semantic-release/changelog": "^6.0.2", 39 | "@semantic-release/commit-analyzer": "^9.0.2", 40 | "@semantic-release/git": "^10.0.1", 41 | "@semantic-release/npm": "^9.0.1", 42 | "@semantic-release/release-notes-generator": "^10.0.3", 43 | "@types/jest": "^29.2.5", 44 | "@types/qs": "^6.9.7", 45 | "@types/validator": "^13.7.10", 46 | "@typescript-eslint/eslint-plugin": "^5.47.1", 47 | "@typescript-eslint/parser": "^5.47.1", 48 | "eslint": "^8.30.0", 49 | "eslint-config-prettier": "^8.5.0", 50 | "eslint-plugin-prettier": "^4.2.1", 51 | "husky": "^8.0.2", 52 | "jest": "^29.3.1", 53 | "prettier": "^2.8.1", 54 | "terser-webpack-plugin": "^5.3.6", 55 | "ts-jest": "^29.0.3", 56 | "ts-loader": "^8.4.0", 57 | "typescript": "^4.9.4", 58 | "webpack": "^5.75.0", 59 | "webpack-cli": "^5.0.1" 60 | }, 61 | "dependencies": { 62 | "qs": "^6.11.0", 63 | "validator": "^13.7.0" 64 | }, 65 | "lint-staged": { 66 | "**/*.{js,ts}": "prettier --write --ignore-unknown" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/common.ts: -------------------------------------------------------------------------------- 1 | export const getAmountKey = (functionName: string | undefined) => 2 | functionName === "transfer" ? "uint256" : "value"; 3 | 4 | export const assertAmount = (amount: string) => { 5 | const num = Number(amount); 6 | if (!Number.isFinite(num) || num < 0) { 7 | throw new Error(`Invalid amount: "${amount}"`); 8 | } 9 | }; 10 | 11 | export const assertStarknetAddress = (address: string): boolean => { 12 | if (!new RegExp(`^${STARKNET_ADDRESS_REGEX}$`).test(address)) { 13 | throw new Error(`Invalid StarkNet address: "${address}"`); 14 | } 15 | return true; 16 | }; 17 | 18 | export const STARKNET_SCHEMA = "starknet:"; 19 | 20 | /** 21 | * valid StarkNet address regex 22 | */ 23 | export const STARKNET_ADDRESS_REGEX = "0x[0-9a-fA-F]{1,64}"; 24 | 25 | export const STARKNET_ETH = 26 | "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7"; 27 | 28 | export enum StarknetChainId { 29 | // noinspection JSUnusedGlobalSymbols 30 | MAINNET = "0x534e5f4d41494e", 31 | GOERLI = "0x534e5f474f45524c49", 32 | GOERLI2 = "0x534e5f474f45524c4932", 33 | } 34 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import qs from "qs"; 2 | 3 | import type { 4 | BuildOptions, 5 | ChainId, 6 | ParseResult, 7 | Token, 8 | TransferOptions, 9 | } from "./types"; 10 | import { 11 | assertAmount, 12 | assertStarknetAddress, 13 | getAmountKey, 14 | STARKNET_ADDRESS_REGEX, 15 | STARKNET_SCHEMA, 16 | StarknetChainId, 17 | } from "./common"; 18 | import isURL from "validator/lib/isURL"; 19 | 20 | /** 21 | * Parse a StarkNet URI 22 | * 23 | * @param uri StarkNet URI 24 | */ 25 | export const parse = (uri: string): ParseResult => { 26 | // noinspection SuspiciousTypeOfGuard 27 | if (typeof uri !== "string") { 28 | throw new Error(`"uri" must be a string`); 29 | } 30 | 31 | if (!uri.startsWith(STARKNET_SCHEMA)) { 32 | throw new Error(`Invalid schema URI`); 33 | } 34 | 35 | let address_regex = `(${STARKNET_ADDRESS_REGEX})`; 36 | let prefix = undefined; 37 | 38 | if (!uri.startsWith(`${STARKNET_SCHEMA}0x`)) { 39 | // a non-address prefix must end with "-" 40 | const prefixEnd = uri.indexOf("-", STARKNET_SCHEMA.length); 41 | if (prefixEnd === -1) { 42 | throw new Error("Missing prefix"); 43 | } 44 | 45 | prefix = uri.substring(STARKNET_SCHEMA.length, prefixEnd); 46 | 47 | const restOfURI = uri.substring(prefixEnd + 1); 48 | if (!restOfURI.toLowerCase().startsWith("0x")) { 49 | // FIXME ATM there is no clear standard for StarkNet domains, 50 | // the following is a super naive implementation 51 | 52 | // `target_address` is a domain - so we should validate it as one 53 | address_regex = 54 | "([a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\\.[a-zA-Z]{2,})"; 55 | } 56 | } 57 | 58 | const matched = uri.match( 59 | new RegExp( 60 | `^${STARKNET_SCHEMA}(${prefix}-)?${address_regex}\\@?(\\w*)*\\/?(\\w*)*` 61 | ) 62 | ); 63 | if (!matched) { 64 | throw new Error("URI parsing failed"); 65 | } 66 | 67 | const [, , target_address, chain_id, function_name] = matched; 68 | 69 | // `value` type must be either a string or a number 70 | const parameters = Object.entries(qs.parse(uri.split("?")?.[1])).reduce<{ 71 | [key: string]: string | number; 72 | }>( 73 | (params, [key, value]) => ({ 74 | ...params, 75 | [key]: `${value}`, // force string `value` to avoid nested-params/arrays 76 | }), 77 | {} 78 | ); 79 | 80 | const amountKey = getAmountKey(function_name); 81 | if (parameters[amountKey]) { 82 | // `amountKey` value type should be either `string` or `number` 83 | const givenAmount = parameters[amountKey] as string; 84 | 85 | const actualAmount = Number(givenAmount).toString(); 86 | assertAmount(actualAmount); 87 | parameters[amountKey] = actualAmount; 88 | } 89 | 90 | // remove `undefined` keys 91 | return JSON.parse( 92 | JSON.stringify({ 93 | schema: STARKNET_SCHEMA.replace(":", ""), 94 | prefix, 95 | target_address, 96 | chain_id: chain_id as ChainId, 97 | function_name, 98 | parameters: Object.keys(parameters).length ? parameters : undefined, 99 | }) 100 | ); 101 | }; 102 | 103 | /** 104 | * Build a StarkNet URI 105 | * 106 | * @param options 107 | */ 108 | export const build = (options: BuildOptions): string => { 109 | const { prefix, target_address, chain_id, function_name, parameters } = 110 | options; 111 | 112 | if (!target_address) { 113 | throw new Error(`"target_address" must be defined`); 114 | } 115 | 116 | let queryParams; 117 | if (parameters) { 118 | const amountKey = getAmountKey(function_name); 119 | if (parameters[amountKey]) { 120 | const givenAmount = parameters[amountKey]; 121 | const actualAmount = Number(givenAmount) 122 | .toLocaleString("en-US", { 123 | notation: "scientific", 124 | maximumFractionDigits: 20, 125 | }) 126 | .replace(/e0/i, "") 127 | .toLowerCase(); 128 | assertAmount(actualAmount); 129 | parameters[amountKey] = actualAmount; 130 | } 131 | queryParams = qs.stringify(parameters); 132 | } 133 | 134 | let url = STARKNET_SCHEMA; 135 | if (prefix) url += `${prefix}-`; // i.e. "pay-" 136 | url += target_address; 137 | if (chain_id) url += `@${chain_id}`; 138 | if (function_name) url += `/${function_name}`; 139 | if (queryParams) url += `?${queryParams}`; 140 | 141 | return url; 142 | }; 143 | 144 | /** 145 | * Generate a "dapp" StarkNet URI 146 | * 147 | * @param url dapp url 148 | */ 149 | export const dapp = (url: string): string => { 150 | if (!isURL(url, { protocols: ["https", "http"], require_protocol: true })) { 151 | throw new Error(`Invalid url: "${url}"`); 152 | } 153 | 154 | return build({ 155 | prefix: "dapp", 156 | target_address: url.replace(/(https?):\/\//, ""), 157 | }); 158 | }; 159 | 160 | /** 161 | * Generate a "transfer" StarkNet URI 162 | * 163 | * @param to_address target address 164 | * @param options - `token` to be used by this transfer, 165 | * `amount` requested (optional) 166 | */ 167 | export const transfer = ( 168 | to_address: string, 169 | options: TransferOptions 170 | ): string => { 171 | assertStarknetAddress(to_address); 172 | 173 | const { token, amount } = options; 174 | assertStarknetAddress(token.token_address); 175 | if (!token.chainId) { 176 | throw new Error(`Missing "chainId"`); 177 | } 178 | 179 | const parameters: { [key: string]: string } = { address: to_address }; 180 | if (amount) { 181 | parameters[getAmountKey("transfer")] = `${amount}`; 182 | } 183 | 184 | return build({ 185 | // deliberately skipping the legacy "pay-" prefix, 186 | // the "transfer" `function_name` is clear enough 187 | // prefix: "pay", 188 | 189 | target_address: token.token_address, 190 | chain_id: token.chainId, 191 | function_name: "transfer", 192 | parameters, 193 | }); 194 | }; 195 | 196 | /** 197 | * Generate a "watchAsset" StarkNet URI for watching the given token 198 | * 199 | * @param token to be added by this watchAsset request 200 | */ 201 | export const addToken = (token: Token): string => { 202 | assertStarknetAddress(token.token_address); 203 | if (!token.chainId) { 204 | throw new Error(`Missing "chainId"`); 205 | } 206 | 207 | return build({ 208 | target_address: token.token_address, 209 | chain_id: token.chainId, 210 | function_name: "watchAsset", 211 | parameters: { type: "ERC20" }, 212 | }); 213 | }; 214 | 215 | export { StarknetChainId, STARKNET_SCHEMA }; 216 | export type { BuildOptions, ChainId, ParseResult, Token, TransferOptions }; 217 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { StarknetChainId } from "./common"; 2 | 3 | export type ChainId = StarknetChainId | string | number; 4 | 5 | export type ParseResult = { 6 | schema: string; 7 | prefix?: string; 8 | target_address: string; 9 | chain_id?: ChainId; 10 | function_name?: string; 11 | parameters?: { [key: string]: string }; 12 | }; 13 | 14 | export type BuildOptions = Omit; 15 | 16 | export type Token = { token_address: string; chainId: ChainId }; 17 | 18 | export type TransferOptions = { 19 | token: Token; 20 | amount?: string | number; 21 | }; 22 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { addToken, build, dapp, parse, transfer } from "../src"; 2 | import { 3 | assertAmount, 4 | assertStarknetAddress, 5 | getAmountKey, 6 | STARKNET_ETH, 7 | STARKNET_SCHEMA, 8 | } from "../src/common"; 9 | 10 | const STARKNET_TEST_ACCOUNT = 11 | "0x0603202200000000000000000000000000000000000000000000000000001015"; 12 | 13 | describe("parse", () => { 14 | it("should parse URI with payload starting with `0x`", () => { 15 | expect(parse(`${STARKNET_SCHEMA}${STARKNET_TEST_ACCOUNT}`)).toEqual({ 16 | schema: "starknet", 17 | target_address: STARKNET_TEST_ACCOUNT, 18 | }); 19 | }); 20 | 21 | it("should parse URI with payload starting with `0x` and `pay` prefix", () => { 22 | expect(parse(`${STARKNET_SCHEMA}pay-${STARKNET_TEST_ACCOUNT}`)).toEqual( 23 | { 24 | schema: "starknet", 25 | prefix: "pay", 26 | target_address: STARKNET_TEST_ACCOUNT, 27 | } 28 | ); 29 | }); 30 | 31 | it("should parse URI with payload starting with `0x` and `foo` prefix", () => { 32 | expect(parse(`${STARKNET_SCHEMA}foo-${STARKNET_TEST_ACCOUNT}`)).toEqual( 33 | { 34 | schema: "starknet", 35 | prefix: "foo", 36 | target_address: STARKNET_TEST_ACCOUNT, 37 | } 38 | ); 39 | }); 40 | 41 | it("should parse URI with a domain name", () => { 42 | expect( 43 | parse(`${STARKNET_SCHEMA}foo-first-sword-of-braavos.stark`) 44 | ).toEqual({ 45 | schema: "starknet", 46 | prefix: "foo", 47 | target_address: "first-sword-of-braavos.stark", 48 | }); 49 | }); 50 | 51 | it("should parse URI with chain id", () => { 52 | expect( 53 | parse(`${STARKNET_SCHEMA}${STARKNET_TEST_ACCOUNT}@SN_GOERLI`) 54 | ).toEqual({ 55 | schema: "starknet", 56 | target_address: STARKNET_TEST_ACCOUNT, 57 | chain_id: "SN_GOERLI", 58 | }); 59 | }); 60 | 61 | it("should parse an ERC20 token transfer", () => { 62 | expect( 63 | parse( 64 | `${STARKNET_SCHEMA}${STARKNET_ETH}/transfer?address=${STARKNET_TEST_ACCOUNT}&uint256=1` 65 | ) 66 | ).toEqual({ 67 | schema: "starknet", 68 | target_address: STARKNET_ETH, 69 | function_name: "transfer", 70 | parameters: { 71 | address: STARKNET_TEST_ACCOUNT, 72 | uint256: "1", 73 | }, 74 | }); 75 | }); 76 | 77 | it("should parse a url with value and gas parameters", () => { 78 | expect( 79 | parse( 80 | `${STARKNET_SCHEMA}${STARKNET_ETH}?value=2.014e18&gas=10&gasLimit=21000&gasPrice=50` 81 | ) 82 | ).toEqual({ 83 | schema: "starknet", 84 | target_address: STARKNET_ETH, 85 | parameters: { 86 | value: "2014000000000000000", 87 | gas: "10", 88 | gasLimit: "21000", 89 | gasPrice: "50", 90 | }, 91 | }); 92 | }); 93 | }); 94 | 95 | describe("build", () => { 96 | it("should build a URL with payload starting with `0x`", () => { 97 | expect( 98 | build({ 99 | target_address: STARKNET_TEST_ACCOUNT, 100 | }) 101 | ).toEqual(`${STARKNET_SCHEMA}${STARKNET_TEST_ACCOUNT}`); 102 | }); 103 | 104 | it("should build a URL with payload starting with `0x` and `pay` prefix", () => { 105 | expect( 106 | build({ 107 | prefix: "pay", 108 | target_address: STARKNET_TEST_ACCOUNT, 109 | }) 110 | ).toEqual(`${STARKNET_SCHEMA}pay-${STARKNET_TEST_ACCOUNT}`); 111 | }); 112 | 113 | it("should build a URL with payload starting with `0x` and `foo` prefix", () => { 114 | expect( 115 | build({ 116 | prefix: "foo", 117 | target_address: STARKNET_TEST_ACCOUNT, 118 | }) 119 | ).toEqual(`${STARKNET_SCHEMA}foo-${STARKNET_TEST_ACCOUNT}`); 120 | }); 121 | 122 | it("should build a URL with a domain name", () => { 123 | expect( 124 | build({ 125 | prefix: "foo", 126 | target_address: "first-sword-of-braavos.stark", 127 | }) 128 | ).toEqual(`${STARKNET_SCHEMA}foo-first-sword-of-braavos.stark`); 129 | }); 130 | 131 | it("should build a URL with chain id", () => { 132 | expect( 133 | build({ 134 | target_address: STARKNET_TEST_ACCOUNT, 135 | chain_id: "SN_GOERLI", 136 | }) 137 | ).toEqual(`${STARKNET_SCHEMA}${STARKNET_TEST_ACCOUNT}@SN_GOERLI`); 138 | }); 139 | 140 | it("should build a URL for an ERC20 token transfer", () => { 141 | expect( 142 | build({ 143 | target_address: STARKNET_ETH, 144 | function_name: "transfer", 145 | parameters: { 146 | address: STARKNET_TEST_ACCOUNT, 147 | uint256: "1", 148 | }, 149 | }) 150 | ).toEqual( 151 | `${STARKNET_SCHEMA}${STARKNET_ETH}/transfer?address=${STARKNET_TEST_ACCOUNT}&uint256=1` 152 | ); 153 | }); 154 | 155 | it("should build a url with value and gas parameters", () => { 156 | expect( 157 | build({ 158 | target_address: STARKNET_ETH, 159 | parameters: { 160 | value: "2014000000000000000", 161 | gas: "10", 162 | gasLimit: "21000", 163 | gasPrice: "50", 164 | }, 165 | }) 166 | ).toEqual( 167 | `${STARKNET_SCHEMA}${STARKNET_ETH}?value=2.014e18&gas=10&gasLimit=21000&gasPrice=50` 168 | ); 169 | }); 170 | }); 171 | 172 | describe("common", () => { 173 | it("should return correct amount-key for `foo`", function () { 174 | expect(getAmountKey("foo")).toEqual("value"); 175 | }); 176 | 177 | it("should return correct amount-key for undefined", function () { 178 | expect(getAmountKey(undefined)).toEqual("value"); 179 | }); 180 | 181 | it("should return correct amount-key for `transfer`", function () { 182 | expect(getAmountKey("transfer")).toEqual("uint256"); 183 | }); 184 | 185 | it("should assert-amount for undefined", function () { 186 | // @ts-expect-error TS2345 we want to test raw js access 187 | expect(() => assertAmount(undefined)).toThrow(); 188 | }); 189 | 190 | it("should assert-amount for text", function () { 191 | expect(() => assertAmount("foo")).toThrow(); 192 | }); 193 | 194 | it("should assert-amount for negative number-string", function () { 195 | expect(() => assertAmount("-1")).toThrow(); 196 | }); 197 | 198 | it("should assert-amount for negative number", function () { 199 | // @ts-expect-error TS2345 we want to test raw js access 200 | expect(() => assertAmount(-1)).toThrow(); 201 | }); 202 | 203 | it("should assert-amount for empty string", function () { 204 | expect(() => assertAmount("")).not.toThrow(); 205 | }); 206 | 207 | it("should assert-amount for number", function () { 208 | // @ts-expect-error TS2345 we want to test raw js access 209 | expect(() => assertAmount(0.1)).not.toThrow(); 210 | }); 211 | 212 | it("should assert-amount for number-string", function () { 213 | expect(() => assertAmount("0.1e18")).not.toThrow(); 214 | }); 215 | 216 | it("should assert-address for undefined", function () { 217 | // @ts-expect-error TS2345 we want to test raw js access 218 | expect(() => assertStarknetAddress(undefined)).toThrow(); 219 | }); 220 | 221 | it("should assert-address for text", function () { 222 | expect(() => assertStarknetAddress("foo")).toThrow(); 223 | }); 224 | 225 | it("should assert-address for invalid prefixes", function () { 226 | expect(() => assertStarknetAddress(" 0x0")).toThrow(); 227 | expect(() => assertStarknetAddress("-0x0")).toThrow(); 228 | expect(() => assertStarknetAddress("\n0x0")).toThrow(); 229 | }); 230 | 231 | it("should assert-address for empty string", function () { 232 | expect(() => assertStarknetAddress("")).toThrow(); 233 | }); 234 | 235 | it("should assert-address for number", function () { 236 | // @ts-expect-error TS2345 we want to test raw js access 237 | expect(() => assertStarknetAddress(0.1)).toThrow(); 238 | }); 239 | 240 | it("should assert-address for too short", function () { 241 | expect(() => assertStarknetAddress("0x")).toThrow(); 242 | }); 243 | 244 | it("should assert-address for too long", function () { 245 | expect(() => 246 | assertStarknetAddress(`${STARKNET_TEST_ACCOUNT}000`) 247 | ).toThrow(); 248 | }); 249 | 250 | it("should assert-address for short", function () { 251 | expect(() => assertStarknetAddress("0x0")).toBeTruthy(); 252 | }); 253 | 254 | it("should assert-address for long", function () { 255 | expect(() => assertStarknetAddress(STARKNET_TEST_ACCOUNT)).toBeTruthy(); 256 | }); 257 | }); 258 | 259 | describe("dapp", () => { 260 | it("should handle https url", function () { 261 | expect(dapp("https://www.example.com/#/")).toEqual( 262 | `${STARKNET_SCHEMA}dapp-www.example.com/#/` 263 | ); 264 | }); 265 | 266 | it("should handle http url", function () { 267 | // noinspection HttpUrlsUsage 268 | expect(dapp("http://example.com?q=1")).toEqual( 269 | `${STARKNET_SCHEMA}dapp-example.com?q=1` 270 | ); 271 | }); 272 | 273 | it("should keep qs and hash", function () { 274 | expect(dapp("https://example.com/#foo?q=1")).toEqual( 275 | `${STARKNET_SCHEMA}dapp-example.com/#foo?q=1` 276 | ); 277 | }); 278 | 279 | it("should throw on invalid protocol", function () { 280 | expect(() => dapp("ftp://example.com")).toThrow(); 281 | }); 282 | 283 | it("should throw on missing protocol", function () { 284 | expect(() => dapp("example.com")).toThrow(); 285 | expect(() => dapp("www.example.com")).toThrow(); 286 | }); 287 | 288 | it("should throw on invalid domain", function () { 289 | expect(() => dapp("example")).toThrow(); 290 | }); 291 | 292 | it("should throw on empty string", function () { 293 | expect(() => dapp("")).toThrow(); 294 | }); 295 | }); 296 | 297 | describe("transfer", () => { 298 | it("should generate a mainnet eth request with no amount", function () { 299 | expect( 300 | transfer(STARKNET_TEST_ACCOUNT, { 301 | token: { token_address: STARKNET_ETH, chainId: "SN_MAIN" }, 302 | }) 303 | ).toEqual( 304 | `${STARKNET_SCHEMA}${STARKNET_ETH}@SN_MAIN/transfer?address=${STARKNET_TEST_ACCOUNT}` 305 | ); 306 | }); 307 | 308 | it("should generate a mainnet eth request with amount", function () { 309 | expect( 310 | transfer(STARKNET_TEST_ACCOUNT, { 311 | token: { token_address: STARKNET_ETH, chainId: "SN_MAIN" }, 312 | amount: 1.1, 313 | }) 314 | ).toEqual( 315 | `${STARKNET_SCHEMA}${STARKNET_ETH}@SN_MAIN/transfer?address=${STARKNET_TEST_ACCOUNT}&uint256=1.1` 316 | ); 317 | }); 318 | 319 | it("should generate a custom token request with no amount", function () { 320 | expect( 321 | transfer(STARKNET_TEST_ACCOUNT, { 322 | token: { token_address: "0x12345", chainId: "SN_GOERLI2" }, 323 | }) 324 | ).toEqual( 325 | `${STARKNET_SCHEMA}0x12345@SN_GOERLI2/transfer?address=${STARKNET_TEST_ACCOUNT}` 326 | ); 327 | }); 328 | 329 | it("should generate a custom token request with amount", function () { 330 | expect( 331 | transfer(STARKNET_TEST_ACCOUNT, { 332 | token: { token_address: "0x12345", chainId: "SN_GOERLI2" }, 333 | amount: "0o377777777777777777", 334 | }) 335 | ).toEqual( 336 | `${STARKNET_SCHEMA}0x12345@SN_GOERLI2/transfer?address=${STARKNET_TEST_ACCOUNT}&uint256=9.007199254740991e15` 337 | ); 338 | 339 | expect( 340 | transfer(STARKNET_TEST_ACCOUNT, { 341 | token: { token_address: "0x12345", chainId: "SN_GOERLI2" }, 342 | amount: "9007199254740991", 343 | }) 344 | ).toEqual( 345 | `${STARKNET_SCHEMA}0x12345@SN_GOERLI2/transfer?address=${STARKNET_TEST_ACCOUNT}&uint256=9.007199254740991e15` 346 | ); 347 | 348 | expect( 349 | transfer(STARKNET_TEST_ACCOUNT, { 350 | token: { token_address: "0x12345", chainId: "SN_GOERLI2" }, 351 | amount: "0x1fffffffffffff", 352 | }) 353 | ).toEqual( 354 | `${STARKNET_SCHEMA}0x12345@SN_GOERLI2/transfer?address=${STARKNET_TEST_ACCOUNT}&uint256=9.007199254740991e15` 355 | ); 356 | 357 | expect( 358 | transfer(STARKNET_TEST_ACCOUNT, { 359 | token: { token_address: "0x12345", chainId: "SN_GOERLI2" }, 360 | amount: "0b11111111111111111111111111111111111111111111111111111", 361 | }) 362 | ).toEqual( 363 | `${STARKNET_SCHEMA}0x12345@SN_GOERLI2/transfer?address=${STARKNET_TEST_ACCOUNT}&uint256=9.007199254740991e15` 364 | ); 365 | }); 366 | 367 | it("should throw on invalid to_address", function () { 368 | expect(() => 369 | transfer("0x", { 370 | token: { token_address: "0x0", chainId: "SN_GOERLI" }, 371 | }) 372 | ).toThrow(); 373 | }); 374 | 375 | it("should throw on invalid token option", function () { 376 | expect(() => 377 | // @ts-expect-error TS2322 we want to test raw js access 378 | transfer(STARKNET_TEST_ACCOUNT, { token: null }) 379 | ).toThrow(); 380 | }); 381 | 382 | it("should throw on invalid token_address", function () { 383 | expect(() => 384 | // @ts-expect-error TS2322 we want to test raw js access 385 | transfer(STARKNET_TEST_ACCOUNT, { token: { token_address: null } }) 386 | ).toThrow(); 387 | }); 388 | 389 | it("should throw on invalid amount", function () { 390 | expect(() => 391 | transfer(STARKNET_TEST_ACCOUNT, { 392 | token: { token_address: STARKNET_ETH, chainId: "SN_GOERLI" }, 393 | amount: "foo", 394 | }) 395 | ).toThrow(); 396 | 397 | expect(() => 398 | transfer(STARKNET_TEST_ACCOUNT, { 399 | token: { token_address: STARKNET_ETH, chainId: "SN_GOERLI" }, 400 | amount: Number.POSITIVE_INFINITY, 401 | }) 402 | ).toThrow(); 403 | 404 | expect(() => 405 | transfer(STARKNET_TEST_ACCOUNT, { 406 | token: { token_address: STARKNET_ETH, chainId: "SN_GOERLI" }, 407 | amount: -1, 408 | }) 409 | ).toThrow(); 410 | }); 411 | }); 412 | 413 | describe("addToken", () => { 414 | it("should generate a watchAsset request for mainnet-ERC20", function () { 415 | expect( 416 | addToken({ token_address: STARKNET_ETH, chainId: "SN_MAIN" }) 417 | ).toEqual( 418 | `${STARKNET_SCHEMA}${STARKNET_ETH}@SN_MAIN/watchAsset?type=ERC20` 419 | ); 420 | }); 421 | 422 | it("should generate a watchAsset request for testnet-ERC20", function () { 423 | expect( 424 | addToken({ token_address: STARKNET_ETH, chainId: "SN_GOERLI" }) 425 | ).toEqual( 426 | `${STARKNET_SCHEMA}${STARKNET_ETH}@SN_GOERLI/watchAsset?type=ERC20` 427 | ); 428 | }); 429 | 430 | it("should generate a watchAsset request url for custom chainId custom ERC20", function () { 431 | expect( 432 | addToken({ token_address: "0x12345", chainId: "fooId" }) 433 | ).toEqual(`${STARKNET_SCHEMA}0x12345@fooId/watchAsset?type=ERC20`); 434 | }); 435 | 436 | it("should throw for invalid options", function () { 437 | // @ts-expect-error TS2322 we want to test raw js access 438 | expect(() => addToken({ token_address: null })).toThrow(); 439 | 440 | expect(() => 441 | addToken({ token_address: "foo", chainId: "SN_MAIN" }) 442 | ).toThrow(); 443 | }); 444 | }); 445 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext"], 5 | "declaration": true, 6 | "outDir": "dist", 7 | "allowJs": false, 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "module": "ESNext", 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "noEmit": false 18 | }, 19 | "exclude": ["node_modules"], 20 | "include": ["src"] 21 | } 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | const __dirname = path.resolve(); 4 | 5 | export default { 6 | mode: process.env.WEBPACK_MODE, 7 | entry: "./src/index.ts", 8 | experiments: { 9 | outputModule: true, 10 | }, 11 | output: { 12 | filename: "index.js", 13 | path: path.resolve(__dirname, "dist"), 14 | library: { 15 | type: "module", 16 | }, 17 | clean: true, 18 | }, 19 | resolve: { 20 | extensions: [".tsx", ".ts", ".js"], 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.tsx?$/, 26 | use: "ts-loader", 27 | exclude: /node_modules/, 28 | }, 29 | ], 30 | }, 31 | }; 32 | --------------------------------------------------------------------------------