├── .circleci └── config.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── README_ORIGINAL.md ├── RELEASING.md ├── babel.config.js ├── config └── polyfills.js ├── documentation ├── .env ├── .gitignore ├── README.md ├── config-overrides.js ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── src │ ├── App.js │ ├── App.test.js │ ├── AppState.js │ ├── README.md │ ├── basics │ │ ├── Block.js │ │ ├── Semibold.js │ │ └── Tooltip.js │ ├── components │ │ ├── Comment.js │ │ ├── DisplayInterface.js │ │ ├── DisplayItem.js │ │ ├── DisplayMethod.js │ │ ├── DisplayParameters.js │ │ ├── DisplayProperty.js │ │ ├── DisplayType.js │ │ ├── LinkToID.js │ │ ├── Markdown.js │ │ ├── TableOfContents.js │ │ └── TypePeeker.js │ ├── docs.json │ ├── helpers │ │ ├── getArmName.js │ │ └── getLink.js │ ├── index.css │ └── index.js └── yarn.lock ├── jest.config.js ├── package.json ├── plans ├── README.md ├── authservers.md ├── data.md ├── general.md ├── keymanager │ ├── data-privacy.md │ ├── overview.md │ └── scenarios.md ├── plugintests.md ├── sep6.md └── sep8.md ├── playground ├── .env ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── src │ ├── @stellar │ │ └── wallet-sdk │ ├── App.js │ ├── components │ │ ├── AccountDetails.js │ │ ├── AuthCurrency.js │ │ ├── Authorization.js │ │ ├── KeyEntry.js │ │ ├── MergeTransaction.js │ │ ├── Offers.js │ │ ├── OperationViewer.js │ │ ├── Payments.js │ │ ├── Trades.js │ │ ├── TransactionViewer.js │ │ └── TransferProvider.js │ └── index.js └── yarn.lock ├── src ├── KeyManager.test.ts ├── KeyManager.ts ├── KeyManagerPlugins.ts ├── PluginTesting.test.ts ├── PluginTesting.ts ├── browser.ts ├── constants │ ├── data.ts │ ├── keys.ts │ ├── sep8.ts │ ├── stellar.ts │ └── transfers.ts ├── data │ ├── DataProvider.test.ts │ ├── DataProvider.ts │ ├── index.test.ts │ ├── index.ts │ ├── makeDisplayableBalances.test.ts │ ├── makeDisplayableBalances.ts │ ├── makeDisplayableOffers.test.ts │ ├── makeDisplayableOffers.ts │ ├── makeDisplayablePayments.test.ts │ ├── makeDisplayablePayments.ts │ ├── makeDisplayableTrades.test.ts │ └── makeDisplayableTrades.ts ├── fixtures │ ├── AccountResponse.ts │ ├── OffersResponse.ts │ ├── PaymentsResponse.ts │ ├── SponsoredAccountResponse.ts │ ├── TradesResponse.ts │ ├── TransactionsResponse.ts │ ├── TransferInfoResponse.ts │ └── keys.ts ├── helpers │ ├── ScryptEncryption.test.ts │ ├── ScryptEncryption.ts │ ├── bigize.test.ts │ ├── bigize.ts │ ├── getKeyMetadata.test.ts │ ├── getKeyMetadata.ts │ └── trezorTransformTransaction.ts ├── index.ts ├── keyTypeHandlers │ ├── albedo.ts │ ├── freighter.test.ts │ ├── freighter.ts │ ├── ledger.ts │ ├── plaintextKey.ts │ └── trezor.ts ├── plugins │ ├── BrowserStorageFacade.ts │ ├── BrowserStorageKeyStore.test.ts │ ├── BrowserStorageKeyStore.ts │ ├── IdentityEncrypter.test.ts │ ├── IdentityEncrypter.ts │ ├── LocalStorageFacade.ts │ ├── LocalStorageKeyStore.test.ts │ ├── LocalStorageKeyStore.ts │ ├── MemoryKeyStore.test.ts │ ├── MemoryKeyStore.ts │ ├── ScryptEncrypter.test.ts │ └── ScryptEncrypter.ts ├── sep8 │ ├── ApprovalProvider.test.ts │ ├── ApprovalProvider.ts │ ├── getApprovalServerUrl.test.ts │ ├── getApprovalServerUrl.ts │ ├── getRegulatedAssetsInTx.ts │ └── getRegulatedAssetsinTx.test.ts ├── testUtils.ts ├── transfers │ ├── DepositProvider.test.ts │ ├── DepositProvider.ts │ ├── TransferProvider.ts │ ├── WithdrawProvider.ts │ ├── fetchKycInBrowser.ts │ ├── getKycUrl.test.ts │ ├── getKycUrl.ts │ ├── index.ts │ ├── parseInfo.test.ts │ └── parseInfo.ts ├── types │ ├── @modules.d.ts │ ├── data.ts │ ├── index.ts │ ├── keys.ts │ ├── modules.d.ts │ ├── sep8.ts │ ├── transfers.ts │ └── watchers.ts └── util.d.ts ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | 7 | defaults: &defaults 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:lts 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | jobs: 20 | build: 21 | <<: *defaults 22 | steps: 23 | - checkout 24 | 25 | # Download and cache dependencies 26 | - restore_cache: 27 | keys: 28 | - v1-dependencies-{{ checksum "package.json" }} 29 | # fallback to using the latest cache if no exact match is found 30 | - v1-dependencies- 31 | 32 | - run: yarn install 33 | 34 | - save_cache: 35 | paths: 36 | - node_modules 37 | key: v1-dependencies-{{ checksum "package.json" }} 38 | 39 | # run tests! 40 | # - run: yarn test:ci 41 | 42 | # build 43 | - run: yarn build 44 | 45 | - persist_to_workspace: 46 | root: ~/repo 47 | paths: . 48 | 49 | deploy: 50 | <<: *defaults 51 | steps: 52 | - attach_workspace: 53 | at: ~/repo 54 | - run: 55 | name: Authenticate with registry 56 | command: 57 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc 58 | - run: 59 | name: Publish package 60 | command: npm publish --access public 61 | 62 | workflows: 63 | version: 2 64 | test-deploy: 65 | jobs: 66 | - build: 67 | filters: 68 | tags: 69 | only: /^v.*/ 70 | - deploy: 71 | requires: 72 | - build 73 | filters: 74 | tags: 75 | only: /^v.*/ 76 | branches: 77 | ignore: /.*/ 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | compiled 4 | *.log 5 | .DS_Store 6 | docs 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # js-stellar-wallets 2 | 3 | > **Warning** 4 | > 5 | > This project has been deprecated in favor of 6 | > . 7 | 8 | To read the original README, please visit 9 | . 10 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing @stellar/wallet-sdk to NPM 2 | 3 | All you have to do is creating a new Github release using the `v` at the 4 | beginning, like in `v0.3.0-rc.6`. 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | global.regeneratorRuntime = require("regenerator-runtime"); 2 | require("jest-fetch-mock").enableMocks(); 3 | -------------------------------------------------------------------------------- /documentation/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | NODE_PATH=./src 3 | -------------------------------------------------------------------------------- /documentation/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /documentation/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with 2 | [Create React App](https://github.com/facebook/create-react-app). 3 | 4 | ## Available Scripts 5 | 6 | In the project directory, you can run: 7 | 8 | ### `npm start` 9 | 10 | Runs the app in the development mode.
Open 11 | [http://localhost:3000](http://localhost:3000) to view it in the browser. 12 | 13 | The page will reload if you make edits.
You will also see any lint errors in 14 | the console. 15 | 16 | ### `npm test` 17 | 18 | Launches the test runner in the interactive watch mode.
See the section 19 | about 20 | [running tests](https://facebook.github.io/create-react-app/docs/running-tests) 21 | for more information. 22 | 23 | ### `npm run build` 24 | 25 | Builds the app for production to the `build` folder.
It correctly bundles 26 | React in production mode and optimizes the build for the best performance. 27 | 28 | The build is minified and the filenames include the hashes.
Your app is 29 | ready to be deployed! 30 | 31 | See the section about 32 | [deployment](https://facebook.github.io/create-react-app/docs/deployment) for 33 | more information. 34 | 35 | ### `npm run eject` 36 | 37 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 38 | 39 | If you aren’t satisfied with the build tool and configuration choices, you can 40 | `eject` at any time. This command will remove the single build dependency from 41 | your project. 42 | 43 | Instead, it will copy all the configuration files and the transitive 44 | dependencies (Webpack, Babel, ESLint, etc) right into your project so you have 45 | full control over them. All of the commands except `eject` will still work, but 46 | they will point to the copied scripts so you can tweak them. At this point 47 | you’re on your own. 48 | 49 | You don’t have to ever use `eject`. The curated feature set is suitable for 50 | small and middle deployments, and you shouldn’t feel obligated to use this 51 | feature. However we understand that this tool wouldn’t be useful if you couldn’t 52 | customize it when you are ready for it. 53 | 54 | ## Learn More 55 | 56 | You can learn more in the 57 | [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 58 | 59 | To learn React, check out the [React documentation](https://reactjs.org/). 60 | 61 | ### Code Splitting 62 | 63 | This section has moved here: 64 | https://facebook.github.io/create-react-app/docs/code-splitting 65 | 66 | ### Analyzing the Bundle Size 67 | 68 | This section has moved here: 69 | https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 70 | 71 | ### Making a Progressive Web App 72 | 73 | This section has moved here: 74 | https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 75 | 76 | ### Advanced Configuration 77 | 78 | This section has moved here: 79 | https://facebook.github.io/create-react-app/docs/advanced-configuration 80 | 81 | ### Deployment 82 | 83 | This section has moved here: 84 | https://facebook.github.io/create-react-app/docs/deployment 85 | 86 | ### `npm run build` fails to minify 87 | 88 | This section has moved here: 89 | https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 90 | -------------------------------------------------------------------------------- /documentation/config-overrides.js: -------------------------------------------------------------------------------- 1 | const rewireStyledComponents = require("react-app-rewire-styled-components"); 2 | 3 | /* config-overrides.js */ 4 | module.exports = function override(config, env) { 5 | config = rewireStyledComponents(config, env, { ssr: false }); 6 | 7 | // 0 is parse 8 | // 1 is eslint pre-loader 9 | // 2 is where we want to add loaders 10 | let didAddLoaders = false; 11 | for (let rule of config.module.rules) { 12 | if (rule.oneOf && !didAddLoaders) { 13 | rule.oneOf.unshift({ 14 | test: /\.md$/, 15 | use: "raw-loader", 16 | }); 17 | didAddLoaders = true; 18 | } 19 | } 20 | 21 | return config; 22 | }; 23 | -------------------------------------------------------------------------------- /documentation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "documentation", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "lodash": "^4.17.19", 7 | "react": "^16.8.6", 8 | "react-dom": "^16.8.6", 9 | "react-json-view": "^1.19.1", 10 | "react-markdown": "^4.0.8", 11 | "react-scripts": "^3.1.1", 12 | "react-syntax-highlighter": "^10.2.1", 13 | "styled-components": "^4.2.0" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": "react-app" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 11", 28 | "not op_mini all" 29 | ], 30 | "devDependencies": { 31 | "raw-loader": "^2.0.0", 32 | "raw.macro": "^0.3.0", 33 | "react-app-rewire-styled-components": "^3.0.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /documentation/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stellar/js-stellar-wallets/5ccb6e8a4bfb26c7254aa4e963600f91da122bc0/documentation/public/favicon.ico -------------------------------------------------------------------------------- /documentation/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | Stellar Wallet SDK Documentation 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /documentation/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /documentation/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { App } from "./App"; 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div"); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /documentation/src/AppState.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useReducer } from "react"; 2 | 3 | export const StateContext = createContext(); 4 | 5 | export const StateProvider = ({ reducer, initialState, children }) => ( 6 | 7 | {children} 8 | 9 | ); 10 | 11 | export const useStateValue = () => useContext(StateContext); 12 | -------------------------------------------------------------------------------- /documentation/src/README.md: -------------------------------------------------------------------------------- 1 | ../../README.md -------------------------------------------------------------------------------- /documentation/src/basics/Block.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Block = styled.div` 4 | margin-left: 20px; 5 | margin-top: 5px; 6 | margin-bottom: 5px; 7 | `; 8 | -------------------------------------------------------------------------------- /documentation/src/basics/Semibold.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Semibold = styled.strong` 4 | font-weight: 600; 5 | `; 6 | -------------------------------------------------------------------------------- /documentation/src/basics/Tooltip.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Tooltip = styled.div` 4 | z-index: 10; 5 | position: absolute; 6 | top: 20px; 7 | left: 50%; 8 | 9 | width: 600px; 10 | 11 | padding: 30px; 12 | background: #f3f3f3; 13 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3); 14 | `; 15 | -------------------------------------------------------------------------------- /documentation/src/components/Comment.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | import { Markdown } from "./Markdown"; 5 | 6 | const El = styled.div` 7 | max-width: 800px; 8 | margin-bottom: 10px; 9 | padding: 20px; 10 | background: white; 11 | margin-top: 30px; 12 | word-break: break-word; 13 | `; 14 | 15 | export const Comment = ({ shortText, text }) => ( 16 | 17 | {shortText} 18 | {text} 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /documentation/src/components/DisplayInterface.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { DisplayItem } from "components/DisplayItem"; 4 | import { TypePeeker } from "components/TypePeeker"; 5 | 6 | import { Block } from "basics/Block"; 7 | 8 | export const DisplayInterface = (params) => { 9 | const { 10 | kindString, 11 | name, 12 | children = [], 13 | extendedTypes = [], 14 | implementedTypes = [], 15 | indexSignature = [], 16 | } = params; 17 | 18 | return ( 19 |
20 |
21 | {kindString === "Enumeration" ? "enum" : kindString.toLowerCase()}{" "} 22 | {name} 23 | {!!implementedTypes.length && 24 | implementedTypes.map((implementedType) => ( 25 | 26 | {" "} 27 | implements 28 | 29 | ))} 30 | {!!extendedTypes.length && 31 | extendedTypes.map((extendedType) => ( 32 | 33 | {" "} 34 | extends 35 | 36 | ))} 37 |
38 | 39 | {indexSignature.map(({ parameters, type }) => ( 40 | 41 | [{parameters[0].name}: 42 | ]: ; 43 | 44 | ))} 45 | 46 | {children 47 | .sort((a, b) => a.sources[0].line - b.sources[0].line) 48 | .filter(({ flags }) => !flags.isProtected && !flags.isPrivate) 49 | .map((child) => ( 50 | 51 | 52 | 53 | ))} 54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /documentation/src/components/DisplayItem.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import Json from "react-json-view"; 4 | 5 | import { Comment } from "components/Comment"; 6 | import { DisplayInterface } from "components/DisplayInterface"; 7 | import { DisplayMethod } from "components/DisplayMethod"; 8 | import { DisplayProperty } from "components/DisplayProperty"; 9 | import { DisplayType } from "components/DisplayType"; 10 | 11 | const HeaderEl = styled.div` 12 | background: #dfdfdf; 13 | display: flex; 14 | justify-content: flex-start; 15 | align-items: flex-end; 16 | padding: 10px 20px; 17 | `; 18 | 19 | const NameEl = styled.h2` 20 | margin: 0; 21 | padding: 0; 22 | font-size: 16px; 23 | margin-right: 15px; 24 | `; 25 | 26 | const LineEl = styled.div` 27 | font-size: 0.8em; 28 | `; 29 | 30 | const LineLinkEl = styled.a` 31 | margin-left: 15px; 32 | `; 33 | 34 | const ChildEl = styled.div` 35 | position: relative; 36 | `; 37 | 38 | const RootEl = styled.div` 39 | position: relative; 40 | margin-bottom: 1.5%; 41 | padding: 20px; 42 | background: #f3f3f3; 43 | `; 44 | 45 | export const DisplayItem = ({ isRootElement, ...params }) => { 46 | let item; 47 | 48 | const El = isRootElement ? RootEl : ChildEl; 49 | 50 | switch (params.kindString) { 51 | case "Function": 52 | case "Method": 53 | case "Constructor": 54 | item = ; 55 | break; 56 | 57 | case "Variable": 58 | case "Property": 59 | case "Enumeration member": 60 | item = ; 61 | break; 62 | 63 | case "Class": 64 | case "Interface": 65 | case "Enumeration": 66 | case "Object literal": 67 | item = ; 68 | break; 69 | 70 | case "Type alias": 71 | item = ; 72 | break; 73 | 74 | default: 75 | item = ; 76 | break; 77 | } 78 | 79 | const { id, name, sources, comment } = params; 80 | 81 | return ( 82 | <> 83 | {isRootElement && ( 84 | 85 | 86 | {name} ({params.kindString}) 87 | 88 | 89 | {sources && ( 90 | 91 | {sources[0].fileName} 92 | 98 | ({sources[0].line}:{sources[0].character}) ↗️ 99 | 100 | 101 | )} 102 | 103 | )} 104 | 105 | 106 | {comment && } 107 | 108 | {item} 109 | 110 | 111 | ); 112 | }; 113 | -------------------------------------------------------------------------------- /documentation/src/components/DisplayMethod.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Semibold } from "basics/Semibold"; 4 | 5 | import { Comment } from "components/Comment"; 6 | import { DisplayParameters } from "components/DisplayParameters"; 7 | import { TypePeeker } from "components/TypePeeker"; 8 | import { LinkToID } from "components/LinkToID"; 9 | 10 | export const DisplayMethod = (params) => { 11 | const { name, signatures = [], implementationOf, flags } = params; 12 | return ( 13 |
14 | {!!signatures.length && 15 | signatures.map(({ id, comment, parameters = [], type }) => ( 16 | 17 | {comment && } 18 | {flags.isPrivate && <>private } 19 | {name}( 20 | ) 21 | {type && ( 22 | <> 23 | : 24 | 25 | )} 26 | {implementationOf && ( 27 | <> 28 | {" "} 29 | (See also{" "} 30 | 31 | {implementationOf.name} 32 | 33 | ) 34 | 35 | )} 36 | 37 | ))} 38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /documentation/src/components/DisplayParameters.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { TypePeeker } from "components/TypePeeker"; 4 | 5 | export const DisplayParameters = ({ parameters }) => { 6 | if (!parameters || !parameters.length) { 7 | return null; 8 | } 9 | return ( 10 | <> 11 | {parameters.map((parameter, i) => ( 12 | 13 | {parameter.name} 14 | {parameter.flags.isOptional && <>?}:{" "} 15 | 16 | {i !== parameters.length - 1 && <>, } 17 | 18 | ))} 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /documentation/src/components/DisplayProperty.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Semibold } from "basics/Semibold"; 4 | 5 | import { TypePeeker } from "components/TypePeeker"; 6 | 7 | export const DisplayProperty = ({ 8 | flags, 9 | kindString, 10 | defaultValue, 11 | name, 12 | type = {}, 13 | }) => { 14 | // if this type is a union and one of the types is undefined, 15 | // mark this shit as OPTIONAL 16 | const isOptional = 17 | type.type === "union" && 18 | !!type.types.filter((t) => t.name === "undefined").length; 19 | 20 | return ( 21 |
22 | {flags.isPublic && <>public } 23 | {flags.isPrivate && <>private } 24 | {name} 25 | {isOptional && "?"}:{" "} 26 | {kindString === "Enumeration member" ? ( 27 | {defaultValue} 28 | ) : ( 29 | 34 | )} 35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /documentation/src/components/DisplayType.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { TypePeeker } from "components/TypePeeker"; 4 | 5 | function getType(type) { 6 | switch (type.type) { 7 | case "array": { 8 | return ( 9 | 10 | 11 | [] 12 | 13 | ); 14 | } 15 | 16 | case "union": { 17 | return type.types.reduce((memo, t, i) => { 18 | if (i === type.types.length - 1) { 19 | return [...memo, getType(t)]; 20 | } 21 | 22 | return [ 23 | ...memo, 24 | {getType(t)}, 25 | | , 26 | ]; 27 | }, []); 28 | } 29 | 30 | default: { 31 | return ; 32 | } 33 | } 34 | } 35 | 36 | export const DisplayType = ({ name, type }) => ( 37 |
38 | type {name} = {getType(type)} 39 |
40 | ); 41 | -------------------------------------------------------------------------------- /documentation/src/components/LinkToID.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { getLink } from "helpers/getLink"; 4 | 5 | export const LinkToID = ({ children, id, ...rest }) => ( 6 | item)} 10 | {...rest} 11 | > 12 | {children} 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /documentation/src/components/Markdown.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactMarkdown from "react-markdown"; 3 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; 4 | import { xonokai as theme } from "react-syntax-highlighter/dist/styles/prism"; 5 | 6 | const CodeBlock = ({ language, value }) => ( 7 | 8 | {value} 9 | 10 | ); 11 | 12 | export const Markdown = ({ children }) => ( 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /documentation/src/components/TableOfContents.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | import { getLink } from "helpers/getLink"; 5 | 6 | const El = styled.ul` 7 | margin: 0; 8 | padding: 0; 9 | list-style: none; 10 | `; 11 | 12 | const ListEl = styled.li` 13 | font-size: 0.9em; 14 | margin: 0; 15 | padding: 0; 16 | 17 | &:not(:last-child) { 18 | margin-bottom: 5px; 19 | } 20 | `; 21 | 22 | const AsideEl = styled.em` 23 | display: inline-block; 24 | font-style: normal; 25 | opacity: 0.5; 26 | text-decoration: none; 27 | width: 35px; 28 | text-align: center; 29 | margin-right: 5px; 30 | background: lightGray; 31 | font-size: 0.8em; 32 | `; 33 | 34 | const SHORT_NAME = { 35 | Function: "func", 36 | Method: "func", 37 | Constructor: "func", 38 | Variable: "var", 39 | Property: "prop", 40 | "Enumeration member": "enum", 41 | Class: "class", 42 | Interface: "interf", 43 | Enumeration: "enum", 44 | "Object literal": "obj", 45 | "Type alias": "type", 46 | }; 47 | 48 | export const TableOfContents = ({ items }) => ( 49 |
50 | {Object.keys(items).map((kind) => ( 51 | 52 |

{kind}

53 | 54 | 55 | {items[kind] && 56 | items[kind] 57 | .sort((a, b) => a.name.localeCompare(b.name)) 58 | .map((item) => ( 59 | 60 | 61 | {SHORT_NAME[item.kindString] || item.kindString} 62 | 63 | {item.name} 64 | 65 | ))} 66 | 67 |
68 | ))} 69 |
70 | ); 71 | -------------------------------------------------------------------------------- /documentation/src/components/TypePeeker.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components"; 3 | 4 | import { useStateValue } from "AppState"; 5 | 6 | import { getLink } from "helpers/getLink"; 7 | 8 | import { DisplayItem } from "components/DisplayItem"; 9 | import { DisplayParameters } from "components/DisplayParameters"; 10 | 11 | import { Tooltip } from "basics/Tooltip"; 12 | 13 | const El = styled.span` 14 | position: relative; 15 | `; 16 | 17 | const TypeEl = styled.span` 18 | color: darkBlue; 19 | `; 20 | 21 | const LabelEl = styled.a``; 22 | 23 | export const TypePeeker = ({ 24 | name, 25 | type, 26 | elementType, 27 | types, 28 | typeArguments, 29 | id, 30 | value, 31 | declaration, 32 | }) => { 33 | const [isVisible, toggleVisibility] = useState(false); 34 | 35 | const [{ itemsById }] = useStateValue(); 36 | 37 | if (type === "stringLiteral") { 38 | return "{value}"; 39 | } 40 | 41 | if (type === "reflection" && declaration && declaration.signatures) { 42 | const { signatures } = declaration; 43 | 44 | return ( 45 | <> 46 | () =>{" "} 47 | 48 | 49 | ); 50 | } 51 | 52 | if (type === "union") { 53 | // don't show "undefined" types because that's handled 54 | // by optionalness of the property 55 | return ( 56 | 57 | {types 58 | .filter((t) => t.name !== "undefined") 59 | .map((t, i, arr) => ( 60 | 61 | 62 | {i !== arr.length - 1 && <> | } 63 | 64 | ))} 65 | 66 | ); 67 | } 68 | 69 | if (type === "array") { 70 | // don't show "undefined" types because that's handled 71 | // by optionalness of the property 72 | return ( 73 | 74 | 75 | [] 76 | 77 | ); 78 | } 79 | 80 | if (typeArguments) { 81 | return ( 82 | 83 | {name} 84 | {"<"} 85 | {typeArguments.map((t, i, arr) => ( 86 | 87 | 88 | {i !== arr.length - 1 && <>, } 89 | 90 | ))} 91 | {">"} 92 | 93 | ); 94 | } 95 | 96 | if (!itemsById[id]) { 97 | return {name || "any"}; 98 | } 99 | 100 | return ( 101 | toggleVisibility(true)} 103 | onMouseLeave={() => toggleVisibility(false)} 104 | > 105 | {name || "any"} 106 | 107 | {isVisible && ( 108 | 109 | 110 | 111 | )} 112 | 113 | ); 114 | }; 115 | -------------------------------------------------------------------------------- /documentation/src/docs.json: -------------------------------------------------------------------------------- 1 | ../../dist/docs.json -------------------------------------------------------------------------------- /documentation/src/helpers/getArmName.js: -------------------------------------------------------------------------------- 1 | export function getArmName(path) { 2 | const directories = path.split("/"); 3 | const base = directories[0] === "types" ? directories[1] : directories[0]; 4 | const baseWithoutExtension = base.split(".")[0]; 5 | 6 | return ( 7 | baseWithoutExtension.charAt(0).toUpperCase() + baseWithoutExtension.slice(1) 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /documentation/src/helpers/getLink.js: -------------------------------------------------------------------------------- 1 | export function getLink(id) { 2 | return `#item_${id}`; 3 | } 4 | -------------------------------------------------------------------------------- /documentation/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | body * { 12 | box-sizing: border-box; 13 | } 14 | 15 | code { 16 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 17 | monospace; 18 | } 19 | 20 | p, 21 | ul { 22 | margin-top: 0; 23 | } 24 | 25 | p:last-child, 26 | ul:last-child { 27 | margin-bottom: 0; 28 | } 29 | -------------------------------------------------------------------------------- /documentation/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import { App } from "./App"; 5 | 6 | ReactDOM.render(, document.getElementById("root")); 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | preset: "ts-jest", 6 | transform: { 7 | "^.+\\.(ts|tsx)?$": "ts-jest", 8 | "^.+\\.(js|jsx)$": "babel-jest", 9 | }, 10 | 11 | // Automatically clear mock calls and instances between every test 12 | clearMocks: true, 13 | automock: false, 14 | 15 | setupFiles: ["/config/polyfills.js"], 16 | 17 | // The directory where Jest should output its coverage files 18 | coverageDirectory: "coverage", 19 | 20 | // The test environment that will be used for testing 21 | testEnvironment: "node", 22 | 23 | coveragePathIgnorePatterns: [ 24 | "node_modules", 25 | "documentation", 26 | "playground", 27 | "build", 28 | "dist", 29 | ], 30 | 31 | modulePathIgnorePatterns: ["documentation", "playground", "build", "dist"], 32 | 33 | testPathIgnorePatterns: [ 34 | "node_modules", 35 | "documentation", 36 | "playground", 37 | "build", 38 | "dist", 39 | ], 40 | }; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stellar/wallet-sdk", 3 | "version": "0.11.2", 4 | "description": "Libraries to help you write Stellar-enabled wallets in Javascript", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "repository": "git@github.com:stellar/js-stellar-wallets.git", 8 | "author": "Stellar Development Foundation ", 9 | "license": "Apache-2.0", 10 | "prettier": "@stellar/prettier-config", 11 | "peerDependencies": { 12 | "@stellar/stellar-sdk": "^11.1.0", 13 | "bignumber.js": "*" 14 | }, 15 | "scripts": { 16 | "prepare": "yarn build ; yarn build:commonjs", 17 | "build": "tsc -p tsconfig.json", 18 | "build:commonjs": "webpack --mode production", 19 | "dev": "tsc-watch --project tsconfig.json --onSuccess 'yarn lint'", 20 | "docs": "typedoc", 21 | "lint": "tslint --fix --format verbose --project tsconfig.json", 22 | "lintAndTest": "yarn lint && jest", 23 | "prettier": "prettier --write '**/*.{js,ts,md}'", 24 | "test": "jest --watch", 25 | "test:ci": "jest" 26 | }, 27 | "husky": { 28 | "hooks": { 29 | "pre-commit": "concurrently 'lint-staged' 'yarn build'" 30 | } 31 | }, 32 | "lint-staged": { 33 | "concurrent": true, 34 | "linters": { 35 | "**/*.{js,md}": [ 36 | "prettier --write", 37 | "git add" 38 | ], 39 | "**/*.ts": [ 40 | "prettier --write", 41 | "tslint --fix --format verbose", 42 | "git add" 43 | ] 44 | } 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "^7.23.5", 48 | "@babel/preset-env": "^7.23.8", 49 | "@babel/preset-typescript": "^7.23.3", 50 | "@stellar/prettier-config": "^1.0.1", 51 | "@stellar/tsconfig": "^1.0.2", 52 | "@stellar/tslint-config": "^1.0.4", 53 | "@types/ledgerhq__hw-transport-u2f": "^4.21.1", 54 | "@types/node-localstorage": "^1.3.3", 55 | "@types/sinon": "^7.0.11", 56 | "babel-jest": "^29.7.0", 57 | "babel-loader": "^9.1.3", 58 | "bignumber.js": "^8.1.1", 59 | "concurrently": "^4.1.1", 60 | "husky": "^1.3.1", 61 | "jest": "^29.7.0", 62 | "jest-fetch-mock": "^3.0.3", 63 | "jest-mock-random": "^1.1.1", 64 | "lint-staged": "^8.1.5", 65 | "node-localstorage": "^3.0.5", 66 | "path": "^0.12.7", 67 | "prettier": "^1.17.0", 68 | "regenerator-runtime": "^0.14.0", 69 | "sinon": "^7.3.1", 70 | "terser-webpack-plugin": "^5.3.9", 71 | "ts-loader": "^9.5.1", 72 | "tsc-watch": "^2.1.2", 73 | "tslint": "^5.14.0", 74 | "typedoc": "^0.14.2", 75 | "typescript": "^4.9.5", 76 | "webpack": "^5.89.0", 77 | "webpack-cli": "^5.1.4" 78 | }, 79 | "dependencies": { 80 | "@albedo-link/intent": "^0.9.2", 81 | "@ledgerhq/hw-app-str": "^5.28.0", 82 | "@ledgerhq/hw-transport-u2f": "^5.36.0-deprecated", 83 | "@stellar/freighter-api": "^1.7.1", 84 | "@stellar/stellar-sdk": "^11.1.0", 85 | "@types/jest": "^24.0.11", 86 | "change-case": "^3.1.0", 87 | "lodash": "^4.17.21", 88 | "query-string": "^6.4.2", 89 | "scrypt-async": "^2.0.1", 90 | "trezor-connect": "^8.2.12", 91 | "ts-jest": "^29.1.1", 92 | "tslib": "^2.6.2", 93 | "tweetnacl": "^1.0.3", 94 | "tweetnacl-util": "^0.15.1" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /plans/README.md: -------------------------------------------------------------------------------- 1 | # Plans 2 | 3 | Our notes for planning and implementing the library. 4 | -------------------------------------------------------------------------------- /plans/authservers.md: -------------------------------------------------------------------------------- 1 | # SEP-10 client authentication 2 | 3 | ## Overview 4 | 5 | Provide an API that guides a developer along the process of getting an auth 6 | token for authenticated deposits / withdraws. 7 | 8 | High-level flow: 9 | 10 | - Dev requests supported assets 11 | - The response indicates some or all assets require authentication 12 | - The dev instantiates a class with the authentication server URL 13 | - It sends the user's public key to the authserver 14 | - The authserver responds with a transaction XDR 15 | - The dev signs the transaction with the user's stellar account 16 | - POST the signed tx back to the authserver, and return the JWT 17 | 18 | ## References 19 | 20 | https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md 21 | 22 | ## Basic API 23 | 24 | ```ts 25 | interface GetAuthTokenParams { 26 | authServer: string; 27 | password: string; 28 | publicKey: PublicKey; 29 | } 30 | 31 | const jwt = await KeyManager.fetchAuthToken(params: GetAuthTokenParams); 32 | ``` 33 | -------------------------------------------------------------------------------- /plans/data.md: -------------------------------------------------------------------------------- 1 | # Data API 2 | 3 | The Data API is meant to return readable, understandable processed Stellar data 4 | from the network. 5 | 6 | Some things we did to make the data more understandable: 7 | 8 | - Trade data is from the point of view of the account you use to initiate the 9 | `DataProvider`. That means instead of indicating "sender" and "receiver", 10 | either of which could be your account, trades indicate a "payment token" (what 11 | you send) and an "incoming token" (what you receive). 12 | 13 | ## API surface 14 | 15 | ```typescript 16 | const StellarWallets = { 17 | Data: { 18 | Types: { 19 | Token, 20 | Issuer, 21 | Offer, 22 | Trade, 23 | Payment, 24 | Balance, 25 | NativeBalance, 26 | }, 27 | 28 | // stateless functions 29 | getTokenIdentifier, 30 | 31 | // this is a class, so you can set the Horizon server 32 | // we want to hit 33 | DataProvider: { 34 | // non-fetch functions 35 | isValidKey, 36 | getAccountKey, 37 | 38 | // fetch functions 39 | fetchOpenOffers, 40 | fetchTrades, 41 | fetchPayments, 42 | fetchEffects, 43 | fetchAccountDetails, 44 | 45 | // watchers 46 | watchOpenOffers, 47 | watchTrades, 48 | watchPayments, 49 | fetchEffects, 50 | watchAccountDetails, 51 | }, 52 | }, 53 | }; 54 | ``` 55 | 56 | ## Consistent data shapes 57 | 58 | ```typescript 59 | interface Account { 60 | publicKey: string; 61 | } 62 | 63 | interface Token { 64 | type: string; // or enum? 65 | code: string; 66 | issuer: Issuer; 67 | anchorAsset: string; 68 | numAccounts: Big; 69 | amount: Big; 70 | bidCount: number; 71 | askCount: number; 72 | spread: Big; 73 | } 74 | 75 | interface Issuer { 76 | key: string; 77 | name: string; 78 | url: string; 79 | hostName: string; 80 | } 81 | 82 | interface Effect { 83 | id: string; 84 | senderToken: Token; 85 | receiverToken: Token; 86 | senderAccount: Account; 87 | receiverAccount: Account; 88 | senderAmount: Big; 89 | receiverAmount: Big; 90 | timestamp: number; 91 | } 92 | 93 | interface Trade extends Effect {} 94 | 95 | /* 96 | Offers look strange when partially-filled, I think their IDs change? Might need 97 | to do some work to hide unimportant implementation details from users. 98 | 99 | This might need to be a class instead of an object to support changes, or we 100 | could use Object.defineProperty so users can detect changes: 101 | 102 | https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty 103 | */ 104 | interface Offer extends Effect {} 105 | 106 | interface Payment extends Effect { 107 | paymentType: string; // or enum? basically, is this the "create account" payment 108 | } 109 | 110 | interface Balance { 111 | token: Token; 112 | sellingLiabilities: Big; 113 | 114 | // for non-native tokens, this should be total - sellingLiabilities 115 | // for native, it should also subtract the minimumBalance 116 | available: Big; 117 | total: Big; 118 | } 119 | 120 | // native XLM is similar to Balance 121 | interface NativeBalance extends Balance { 122 | minimumBalance: Big; 123 | } 124 | ``` 125 | 126 | ## Stateless, side-effect-less helpers 127 | 128 | ### Get identifier for a token 129 | 130 | This is useful for people to test token equality, create indices, etc. 131 | 132 | ```typescript 133 | function getTokenIdentifier(token: Token): string { 134 | return `#{token.code}:#{token.issuer.key}`; 135 | } 136 | ``` 137 | 138 | ## Getters and watchers 139 | 140 | ### Get a list of outstanding buy / sell offers for a given token 141 | 142 | ```typescript 143 | function fetchOpenOffers(tokenIdentifier: string): Promise; 144 | 145 | function watchOpenOffers({ 146 | onPartialFill: (offers: Offers[]): void, 147 | 148 | /* 149 | These two get complicated: if an offer gets removed, 150 | it's not an open offer anymore. So we have a couple options: 151 | * Return `offers` and `trades` (the latter isn't a complete list) 152 | * Return `offers`, some of which may be completed 153 | */ 154 | 155 | onComplete: (offers: Offers[]): void, 156 | onCancel: (offers: Offers[]): void, 157 | }): void; 158 | 159 | ``` 160 | 161 | ### Fetch lists of specific data 162 | 163 | Data types: 164 | 165 | - Trades: Two people exchange tokens 166 | - Payments: One person sends a token to another 167 | - Effects: All operations that change an account (like changing trust, changing 168 | inflation destination, etc.) 169 | 170 | ### Get a list of tokens you own with total owned, available balance 171 | 172 | ```typescript 173 | function fetchAccountDetails(): Promise> 174 | 175 | // the watcher returns a function that you can run to cancel the watcher 176 | function watchAccountDtails({ 177 | onMessage: (balances) => Array, 178 | }): function 179 | ``` 180 | -------------------------------------------------------------------------------- /plans/general.md: -------------------------------------------------------------------------------- 1 | ## Design goals 2 | 3 | The overall goal of this library is **to make it easy to add wallet 4 | functionality to an app**. 5 | 6 | This library is meant to prioritize **ease of use**. That means: 7 | 8 | - Design decisions should be anchored by specific and common use cases. 9 | - Types, functions, attributes, etc. are named in an internally consistent way. 10 | - Names should prioritize descriptiveness over brevity. 11 | - Names are not always consistent with those in Horizon or the JS SDK. 12 | - We prioritize doing things automatically for the user over the user having to 13 | repeat actions (for example: calculating prices given a "point of view"). 14 | - There should generally be only one obvious way of accomplishing a task. No 15 | name aliases or alternate APIs. 16 | - We will not attempt to completely replace the JS SDK. 17 | 18 | ## Some consistent design decisions 19 | 20 | - When requesting what could be a list of data, we'll almost always return a 21 | _map of ids to objects_, instead of as an array. You'll be in charge of 22 | sorting that information. 23 | -------------------------------------------------------------------------------- /plans/keymanager/data-privacy.md: -------------------------------------------------------------------------------- 1 | # Data Privacy 2 | 3 | ## Overview 4 | 5 | In order to increase privacy on the data stored, the key manager will encrypt 6 | all data before passing it to the keystore. 7 | 8 | The most notable change related to the wallet SDK's API is that keystores won't 9 | receive the public key anymore; only a client-provided id (or a random number) 10 | will be available to operate search actions, like removing a key. 11 | 12 | ## Data format 13 | 14 | ```ts 15 | export interface EncryptingParams { 16 | type: KeyType | string; 17 | publicKey: string; 18 | privateKey: string; 19 | path?: string; 20 | extra?: any; 21 | } 22 | 23 | export interface EncryptedKey { 24 | id: string; 25 | encryptedBlob: string; 26 | salt: string; 27 | encrypterName: string; 28 | creationTime?: number; 29 | modifiedTime?: number; 30 | } 31 | 32 | export interface Key { 33 | type: KeyType | string; 34 | publicKey: string; 35 | privateKey: string; 36 | path?: string; 37 | extra?: any; 38 | id: string; 39 | creationTime?: number; 40 | modifiedTime?: number; 41 | } 42 | 43 | encryptedBlob = base64(encrypt(json(encryptingParams))); 44 | ``` 45 | 46 | The keystore and keymanager will receive the `id` from now on, allowing 47 | operations on a single key (e.g. removing a key). 48 | 49 | ```ts 50 | export interface Encrypter { 51 | name: string; 52 | encrypt(params: EncryptingParams): Promise; 53 | decrypt(params: EncryptedKey): Promise; 54 | } 55 | 56 | export interface KeyStore { 57 | // ... 58 | 59 | loadKey(id: string): Promise; 60 | removeKey(id: string): Promise; 61 | } 62 | 63 | export class KeyManager { 64 | // ... 65 | 66 | removeKey(id: string): Promise; 67 | } 68 | ``` 69 | 70 | ## Integration with keystore.stellar.org 71 | 72 | In order to integrate with keystore.stellar.org, a few changes would be required 73 | to accomodate the SDK's separation of concerns. Right now, the keystore can't 74 | perform any encryption operations, like encrypting the whole keys blob as 75 | keystore.stellar.org expects. These are the 76 | [updated bits of the spec](https://github.com/stellar/go/blob/master/services/keystore/spec.md). 77 | 78 | ```ts 79 | interface EncryptedKeysData { 80 | keysBlob: string; 81 | creationTime: number; 82 | modifiedTime: number; 83 | } 84 | 85 | interface PutKeysRequest { 86 | keysBlob: string; 87 | } 88 | 89 | keysBlob = base64url(json(EncryptedKey[])) 90 | ``` 91 | 92 | The most important aspect of the changes is that `keysBlob` doesn't require 93 | encryption anymore because each key will have its own encrypted blob, as 94 | described by `EncryptedKey`'s `encryptedBlob` property. 95 | 96 | ## Considerations 97 | 98 | With this new encryption format, it's not possible to simple load all keys 99 | metadata and list your Stellar accounts in read-only mode until you're actually 100 | required to load the private key (e.g. signing transactions). Wallets that want 101 | to defer the passphrase request (e.g. asking the user for the encryption 102 | passphrase) will have to store the public keys themselves. 103 | 104 | Changing how keystore.stellar.org works is also suboptimal. Unfortunately, 105 | unless we change how the SDK works in more deep ways (e.g. passing encryption 106 | utilities from the keymanager to the keystore), we can't comply to the current 107 | API. 108 | 109 | ## References 110 | 111 | https://github.com/stellar/go/blob/master/services/keystore/spec.md 112 | -------------------------------------------------------------------------------- /plans/keymanager/overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | ## Summary 4 | 5 | The key management system is a client-side library that is designed to be 6 | modular and extensible beyond all else. It makes it easy for a wallet app to 7 | store different kinds of Stellar keys and their metadata in a variety of 8 | different ways. 9 | 10 | ## Requirements 11 | 12 | - Modularity 13 | - Extensibility 14 | - Support multiple keys 15 | - Support multiple devices 16 | - Support for multiple kinds of keys: 17 | - Ledger/Trezor 18 | - Entered as plaintext 19 | - From key storage file 20 | - Support for multiple key & metadata stores: 21 | - server 22 | - local disk 23 | - Non-custodial 24 | - Encryption / decryption of keys happens on device / client, not in server 25 | - Can sign transactions 26 | - Interface with any external user management system 27 | 28 | ## Non-goals 29 | 30 | - User management (this is handled externally) 31 | - Recovery of key if password is forgotten: 32 | - user should also store key in separate paper wallet 33 | - or just use a Ledger / Trezor 34 | 35 | ## Pluggable components 36 | 37 | Below are various pluggable components that we should be able to handle. 38 | 39 | - Key stores: 40 | - in browser memory (like StellarTerm) 41 | - local storage in browser 42 | - on device in a mobile app 43 | - in a database or blob store on a server 44 | - A system that combines auth & key storage, like 45 | [stellar-wallet-js-sdk](https://github.com/stellar/stellar-wallet-js-sdk) 46 | - encryption algorithms: 47 | - identity (does nothing) 48 | - Keybase / StellarX-style using nacl box/unbox 49 | - Types of keys: 50 | - Ledger / Trezor 51 | - plain text (S...) 52 | - standard file format(s) 53 | [Protocol Issue](https://github.com/stellar/stellar-protocol/issues/198), 54 | [Stellarport's format](https://github.com/stellarport/stellar-keystore/blob/master/audit.pdf) 55 | -------------------------------------------------------------------------------- /plans/keymanager/scenarios.md: -------------------------------------------------------------------------------- 1 | # Scenarios 2 | 3 | This doc explores different kinds of wallets that may want to use the key 4 | management API, and explains how to use the API to deal with the different 5 | scenarios. 6 | 7 | Each scenario must define the following to work with the key management API: 8 | 9 | - one or more key handlers 10 | - one or more encrypters 11 | - a key store 12 | - an auth strategy 13 | 14 | ## Key Handlers 15 | 16 | The key handlers are shared across all the different scenarios, so just define 17 | them up here 18 | 19 | - plaintext key handler 20 | - takes the key object 21 | - creates a Stellar JS SDK KeyPair from it 22 | - signs the transaction with JS SDK sign method 23 | - returns signed transaction 24 | - The Trezor / Ledger handlers 25 | - read the public key and path from key object 26 | - Use Ledger / Trezor JS libraries to transmit transaction and receive signed 27 | transaction 28 | - returns signed transaction 29 | 30 | ## Encrypters 31 | 32 | A few standard encrypters: 33 | 34 | - identity 35 | - does nothing, returns plaintext as the ciphertext 36 | - StellarX-style 37 | - scrypts the password 38 | - encrypts the key with the scrypted password using nacl `box()` 39 | 40 | ## StellarTerm 41 | 42 | Properties 43 | 44 | - local wallet 45 | - no password 46 | - no auth 47 | - Trezor/Ledger support 48 | - no MFA 49 | 50 | Auth strategy: none 51 | 52 | Encrypter: identity 53 | 54 | KeyStore: local device storage 55 | 56 | - initialized with null authToken 57 | - storeKeys/loadKey just store and read from JS memory 58 | - only one key supported 59 | 60 | KeyHandlers: plaintext, Trezor / Ledger 61 | 62 | ## StellarX 63 | 64 | Auth strategy: AWS Cognito 65 | 66 | Encrypter: StellarX-style 67 | 68 | KeyStore: Keys stored in DynamoDB, accessed via AppSync GraphQL 69 | 70 | - initialized with null authToken 71 | - storeKeys/loadKey make authenticated calls to AppSync GraphQL endpoints 72 | - authentication is handled with singleton that makes AppSync calls, so no 73 | need to initialize any authToken in the KeyStore 74 | - multiple keys are supported 75 | - changePassword is handled in the usual way via `loadAllKeys()` and 76 | `storeKeys()` 77 | 78 | KeyHandlers: plaintext, Trezor / Ledger 79 | 80 | ## Keybase 81 | 82 | Auth strategy: Keybase's user management 83 | 84 | Encrypter: Keybase gives each device a different private / public key pair. They 85 | all have equal power, and are called sibling keys (sibkeys). Further, each 86 | device has the publicKey for each other device. The 87 | [KB key exchange doc](https://keybase.io/docs/crypto/key-exchange) explains this 88 | more. 89 | 90 | password: effectively uses this device's public / private key pair 91 | 92 | `secretKey` in `IEncryptedKey`: string-encoded JSON mapping device name to 93 | Stellar key encrypted with the given device's public key. This allows the 94 | `secretKey` to store the Stellar key in a way that any device can decrypt it. 95 | 96 | to encrypt: 97 | 98 | - encrypt the Stellar secret key with the public keys for all devices 99 | - this means the secret key is encrypted multiple ways, one per device 100 | 101 | to decrypt: 102 | 103 | - use the current device's name to look up the right encrypted key from the 104 | `secretKey` JSON 105 | - decrypt it with the per-device privateKey 106 | 107 | KeyStore: stored on KB's servers 108 | 109 | - initialized with whatever auth-token KB uses 110 | - storeKeys/loadKey 111 | - make authenticated calls to KB servers 112 | - multiple keys can be supported (unlike now in KB, I believe) 113 | 114 | KeyHandlers: plaintext, Trezor / Ledger 115 | 116 | Cases to handle: 117 | 118 | - password change 119 | - this doesn't affect the per-device keys, so no need to do anything 120 | - change of keys for device 121 | - this reduces to `changePassword()` in the normal flow, so it is used as 122 | normal, passing the old `privateKey` in for `oldPassword`, and a 123 | string-encoded JSON mapping device name to public key for the `newPassword`. 124 | The map of course contains the new `publicKey` for this device. 125 | - new device provisioned 126 | - called from outside of the Key Manager API 127 | - the provisioner must decrypt all Stellar secret keys 128 | - encrypt them using the new set of device encrypted keys and create a new 129 | `secretKey` entry in each `IEncryptedKey` 130 | - store the result to the server 131 | 132 | ## Stellar Wallet JS SDK 133 | 134 | See [stellar-wallet-js-sdk](https://github.com/stellar/stellar-wallet-js-sdk). 135 | It's a system that combines auth functionality with storing your stellar keys. 136 | 137 | Unfortunately, it can't be straight-forwardly implemented using the KeyManager 138 | API because encryption, auth, and key storage are all lumped together in 139 | functions like `getWallet`. However, if encryption and key storage were exposed 140 | separately with lower-level functions, one could build KeyManager-compatible 141 | plugins. 142 | -------------------------------------------------------------------------------- /plans/plugintests.md: -------------------------------------------------------------------------------- 1 | # Plugin testing arm 2 | 3 | Developers who write their own Encrypter or KeyStore plugins will want to know 4 | that they meet spec. Likewise, we want an easy way to make sure these plugins 5 | handle edge cases properly (in addition to, you know, working). So it makes 6 | sense for the library to export an arm that helps people write unit tests for 7 | their plugins. 8 | 9 | # General description 10 | 11 | `Encrypter` and `KeyStore` plugins are the highest priority to test. 12 | (`KeyTypeHandler` plugins are important too, but we don't expect people to write 13 | these handlers, and they might be difficult to generically test.) 14 | 15 | My idea is for each plugin type to have a function that runs a gamut of tests on 16 | the plugins, and resolves a promise if the plugin is valid, or rejects with an 17 | error object if the plugin is not. 18 | 19 | Another option is to output one test function for each plugin function, but that 20 | seems tedious to implement. 21 | 22 | Pros: 23 | 24 | - It would let people work and test function-by-function instead of having to 25 | write the whole plugin first. 26 | 27 | Cons: 28 | 29 | - It's hard to individually test some functions (like, how do we write a generic 30 | test for encryptKey without also having access to decryptKey?) 31 | 32 | The con might make it impossible to implement the alternative, so one function 33 | per plugin it is. 34 | 35 | # API Shape 36 | 37 | ```javascript 38 | export const PluginTesting = { 39 | testEncrypter(encrypter: Encrypter): Promise {}, 40 | 41 | testKeyStore(keyStore: KeyStore): Promise {}, 42 | }; 43 | ``` 44 | -------------------------------------------------------------------------------- /playground/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | NODE_PATH=./src 3 | -------------------------------------------------------------------------------- /playground/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /playground/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with 2 | [Create React App](https://github.com/facebook/create-react-app). 3 | 4 | ## Available Scripts 5 | 6 | In the project directory, you can run: 7 | 8 | ### `npm start` 9 | 10 | Runs the app in the development mode.
Open 11 | [http://localhost:3000](http://localhost:3000) to view it in the browser. 12 | 13 | The page will reload if you make edits.
You will also see any lint errors in 14 | the console. 15 | 16 | ### `npm test` 17 | 18 | Launches the test runner in the interactive watch mode.
See the section 19 | about 20 | [running tests](https://facebook.github.io/create-react-app/docs/running-tests) 21 | for more information. 22 | 23 | ### `npm run build` 24 | 25 | Builds the app for production to the `build` folder.
It correctly bundles 26 | React in production mode and optimizes the build for the best performance. 27 | 28 | The build is minified and the filenames include the hashes.
Your app is 29 | ready to be deployed! 30 | 31 | See the section about 32 | [deployment](https://facebook.github.io/create-react-app/docs/deployment) for 33 | more information. 34 | 35 | ### `npm run eject` 36 | 37 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 38 | 39 | If you aren’t satisfied with the build tool and configuration choices, you can 40 | `eject` at any time. This command will remove the single build dependency from 41 | your project. 42 | 43 | Instead, it will copy all the configuration files and the transitive 44 | dependencies (Webpack, Babel, ESLint, etc) right into your project so you have 45 | full control over them. All of the commands except `eject` will still work, but 46 | they will point to the copied scripts so you can tweak them. At this point 47 | you’re on your own. 48 | 49 | You don’t have to ever use `eject`. The curated feature set is suitable for 50 | small and middle deployments, and you shouldn’t feel obligated to use this 51 | feature. However we understand that this tool wouldn’t be useful if you couldn’t 52 | customize it when you are ready for it. 53 | 54 | ## Learn More 55 | 56 | You can learn more in the 57 | [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 58 | 59 | To learn React, check out the [React documentation](https://reactjs.org/). 60 | 61 | ### Code Splitting 62 | 63 | This section has moved here: 64 | https://facebook.github.io/create-react-app/docs/code-splitting 65 | 66 | ### Analyzing the Bundle Size 67 | 68 | This section has moved here: 69 | https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 70 | 71 | ### Making a Progressive Web App 72 | 73 | This section has moved here: 74 | https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 75 | 76 | ### Advanced Configuration 77 | 78 | This section has moved here: 79 | https://facebook.github.io/create-react-app/docs/advanced-configuration 80 | 81 | ### Deployment 82 | 83 | This section has moved here: 84 | https://facebook.github.io/create-react-app/docs/deployment 85 | 86 | ### `npm run build` fails to minify 87 | 88 | This section has moved here: 89 | https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 90 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stellar-wallet-app", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@stellar/elements": "^0.0.0", 7 | "@stellar/stellar-sdk": "^11.1.0", 8 | "big.js": "^5.2.2", 9 | "lodash": "^4.17.19", 10 | "moment": "^2.24.0", 11 | "react": "^16.8.5", 12 | "react-dom": "^16.8.5", 13 | "react-json-view": "^1.19.1", 14 | "react-router-dom": "^5.1.2", 15 | "react-scripts": "^3.1.1" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": "react-app" 25 | }, 26 | "browserslist": [ 27 | ">0.2%", 28 | "not dead", 29 | "not ie <= 11", 30 | "not op_mini all" 31 | ], 32 | "devDependencies": { 33 | "styled-components": "^5.1.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /playground/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stellar/js-stellar-wallets/5ccb6e8a4bfb26c7254aa4e963600f91da122bc0/playground/public/favicon.ico -------------------------------------------------------------------------------- /playground/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /playground/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /playground/src/@stellar/wallet-sdk: -------------------------------------------------------------------------------- 1 | ../../../dist -------------------------------------------------------------------------------- /playground/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom"; 3 | import { GlobalStyle } from "@stellar/elements"; 4 | import * as WalletSdk from "@stellar/wallet-sdk"; 5 | import StellarSdk, { Networks } from "@stellar/stellar-sdk"; 6 | 7 | import AccountDetails from "components/AccountDetails"; 8 | import KeyEntry from "components/KeyEntry"; 9 | import Offers from "components/Offers"; 10 | import Trades from "components/Trades"; 11 | import Payments from "components/Payments"; 12 | import TransferProvider from "components/TransferProvider"; 13 | 14 | class App extends Component { 15 | state = { 16 | dataProvider: null, 17 | authToken: null, 18 | isTestnet: false, 19 | }; 20 | 21 | componentDidMount() { 22 | window.StellarSdk = StellarSdk; 23 | window.WalletSdk = WalletSdk; 24 | } 25 | 26 | _setKey = (publicKey, isTestnet) => { 27 | const dataProvider = new WalletSdk.DataProvider({ 28 | serverUrl: isTestnet 29 | ? "https://horizon-testnet.stellar.org/" 30 | : "https://horizon.stellar.org", 31 | accountOrKey: publicKey, 32 | networkPassphrase: isTestnet ? Networks.TESTNET : Networks.PUBLIC, 33 | }); 34 | 35 | this.setState({ 36 | dataProvider, 37 | }); 38 | }; 39 | 40 | render() { 41 | const { dataProvider, authToken } = this.state; 42 | 43 | return ( 44 | 45 |
46 | 47 | 48 |

Key Data

49 | 50 | this.setState({ authToken })} 53 | /> 54 | 55 | {dataProvider && !dataProvider.isValidKey() && ( 56 |

That's an invalid key!

57 | )} 58 | 59 | {dataProvider && dataProvider.isValidKey() && ( 60 |
61 | 80 | 81 | 82 | 83 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 |
102 | )} 103 |
104 |
105 | ); 106 | } 107 | } 108 | 109 | export default App; 110 | -------------------------------------------------------------------------------- /playground/src/components/AccountDetails.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import Json from "react-json-view"; 3 | 4 | import MergeTransaction from "components/MergeTransaction"; 5 | 6 | class AccountDetails extends Component { 7 | state = { 8 | accountDetails: null, 9 | err: null, 10 | updateTimes: [], 11 | streamEnder: null, 12 | isAccountFunded: null, 13 | }; 14 | 15 | componentDidMount() { 16 | if (this.props.dataProvider) { 17 | this._fetchAccountDetails(this.props.dataProvider); 18 | } 19 | } 20 | 21 | componentWillUpdate(nextProps) { 22 | if ( 23 | typeof this.props.dataProvider !== typeof nextProps.dataProvider || 24 | this.props.dataProvider.getAccountKey() !== 25 | nextProps.dataProvider.getAccountKey() 26 | ) { 27 | this._fetchAccountDetails(nextProps.dataProvider); 28 | } 29 | } 30 | 31 | componentWillUnmount() { 32 | if (this.state.streamEnder) { 33 | this.state.streamEnder(); 34 | } 35 | } 36 | 37 | _fetchAccountDetails = (dataProvider) => { 38 | // if there was a previous data provider, kill the 39 | if (this.state.streamEnder) { 40 | this.state.streamEnder(); 41 | } 42 | 43 | this.setState({ 44 | accountDetails: null, 45 | err: null, 46 | updateTimes: [], 47 | streamEnder: null, 48 | isAccountFunded: false, 49 | }); 50 | 51 | dataProvider 52 | .isAccountFunded() 53 | .then((isFunded) => this.setState({ isFunded })); 54 | 55 | const streamEnder = dataProvider.watchAccountDetails({ 56 | onMessage: (accountDetails) => { 57 | this.setState({ 58 | isAccountFunded: true, 59 | accountDetails, 60 | updateTimes: [...this.state.updateTimes, new Date()], 61 | }); 62 | }, 63 | onError: (err) => { 64 | console.log("error: ", err); 65 | this.setState({ err }); 66 | streamEnder(); 67 | }, 68 | }); 69 | 70 | this.setState({ 71 | streamEnder, 72 | }); 73 | }; 74 | 75 | render() { 76 | const { accountDetails, err, updateTimes, isAccountFunded } = this.state; 77 | 78 | return ( 79 |
80 |

Account Details

81 | 82 | {isAccountFunded && ( 83 | 84 | )} 85 | 86 | {!isAccountFunded &&

Account isn't funded yet.

} 87 | {isAccountFunded && ( 88 | <> 89 |
    90 | {updateTimes.map((time) => ( 91 |
  • {time.toString()}
  • 92 | ))} 93 |
94 | {accountDetails && } 95 | 96 | )} 97 | 98 | {err &&

Error: {err.toString()}

} 99 |
100 | ); 101 | } 102 | } 103 | 104 | export default AccountDetails; 105 | -------------------------------------------------------------------------------- /playground/src/components/AuthCurrency.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import styled from "styled-components"; 3 | 4 | import { DepositProvider } from "@stellar/wallet-sdk"; 5 | import { Button, ButtonThemes, Select } from "@stellar/elements"; 6 | 7 | const El = styled.div``; 8 | 9 | class AuthCurrency extends Component { 10 | state = { 11 | amount: "", 12 | params: {}, 13 | res: null, 14 | error: null, 15 | fee: null, 16 | }; 17 | 18 | async componentDidMount() { 19 | const depositProvider = new DepositProvider( 20 | "https://transfer-dot-jewel-api-dev.appspot.com", 21 | ); 22 | this.setState({ depositProvider }); 23 | 24 | try { 25 | const res = await depositProvider.fetchSupportedAssets(); 26 | this.setState({ depositInfo: res }); 27 | } catch (e) { 28 | this.setState({ error: `fetchSupportedAssets: ${e.toString()}` }); 29 | } 30 | } 31 | 32 | _calculateFee = () => { 33 | const { account, asset_code, depositProvider } = this.props; 34 | const { amount } = this.state; 35 | depositProvider 36 | .fetchFinalFee({ 37 | amount, 38 | asset_code, 39 | account, 40 | }) 41 | .then((fee) => this.setState({ fee })) 42 | .catch((e) => this.setState({ error: e.toString() })); 43 | }; 44 | 45 | _submit = () => { 46 | const { 47 | depositProvider, 48 | account, 49 | asset_code, 50 | authentication_required, 51 | } = this.props; 52 | 53 | depositProvider 54 | .deposit({ 55 | amount: this.state.amount, 56 | account, 57 | asset_code, 58 | authentication_required, 59 | ...this.state.params, 60 | }) 61 | .then((res) => this.setState({ res })) 62 | .catch((e) => { 63 | this.setState({ error: e.toString() }); 64 | }); 65 | }; 66 | 67 | render() { 68 | const { asset_code, min_amount, max_amount, fields } = this.props; 69 | const { params, amount, error, fee } = this.state; 70 | const amountFloat = parseFloat(amount); 71 | 72 | return ( 73 | 74 |

{asset_code}

75 | 76 | {error &&

Deposit error: {error}

} 77 | 78 | 89 | 90 | {fee &&

Fee: {fee}

} 91 | 92 | {fields.map(({ name, description, choices }) => ( 93 |
94 |

{name}

95 |

{description}

96 | 97 | {choices && ( 98 | <> 99 | 113 | 114 | )} 115 |
116 | ))} 117 | 118 | {amountFloat > min_amount && (!max_amount || amountFloat < max_amount) && ( 119 | 122 | )} 123 |
124 | ); 125 | } 126 | } 127 | 128 | export default AuthCurrency; 129 | -------------------------------------------------------------------------------- /playground/src/components/Authorization.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import styled from "styled-components"; 3 | import Json from "react-json-view"; 4 | 5 | import { DepositProvider } from "@stellar/wallet-sdk"; 6 | 7 | import AuthCurrency from "./AuthCurrency"; 8 | 9 | const El = styled.div` 10 | display: flex; 11 | `; 12 | 13 | class Authorization extends Component { 14 | state = { 15 | depositProvider: null, 16 | depositInfo: null, 17 | error: null, 18 | }; 19 | 20 | async componentDidMount() { 21 | const depositProvider = new DepositProvider( 22 | "https://transfer-dot-jewel-api-dev.appspot.com", 23 | ); 24 | 25 | depositProvider.setAuthToken("testtesttest"); 26 | 27 | this.setState({ depositProvider }); 28 | 29 | try { 30 | const res = await depositProvider.fetchSupportedAssets(); 31 | this.setState({ depositInfo: res }); 32 | } catch (e) { 33 | this.setState({ error: `fetchSupportedAssets: ${e.toString()}` }); 34 | } 35 | } 36 | 37 | render() { 38 | const { depositInfo, depositProvider } = this.state; 39 | const account = "GCKM4UPHDEDDFRAR3DWQTUYIFDXB5JG4ARVI56RFEKUEBLTTPRT6AXHA"; 40 | 41 | if (!depositInfo) { 42 | return
Loading...
; 43 | } 44 | 45 | return ( 46 | 47 | 48 | 49 |
50 | {Object.keys(depositInfo).map((currency) => ( 51 | 57 | ))} 58 |
59 |
60 | ); 61 | } 62 | } 63 | 64 | export default Authorization; 65 | -------------------------------------------------------------------------------- /playground/src/components/MergeTransaction.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { StrKey } from "@stellar/stellar-sdk"; 3 | 4 | import TransactionViewer from "components/TransactionViewer"; 5 | 6 | const STORAGE_KEY = "merge-transaction-destination"; 7 | 8 | class MergeTransaction extends Component { 9 | state = { 10 | destination: localStorage.getItem(STORAGE_KEY), 11 | tx: null, 12 | }; 13 | 14 | render() { 15 | const { destination, tx } = this.state; 16 | 17 | const handleDestination = (ev) => { 18 | const dest = ev.target.value; 19 | if (StrKey.isValidEd25519PublicKey(dest)) { 20 | localStorage.setItem(STORAGE_KEY, dest); 21 | } 22 | this.setState({ destination: dest }); 23 | }; 24 | 25 | const handleGetTransaction = async () => { 26 | try { 27 | const trans = await this.props.dataProvider.getStripAndMergeAccountTransaction( 28 | destination, 29 | ); 30 | this.setState({ tx: trans }); 31 | } catch (e) { 32 | debugger; 33 | } 34 | }; 35 | 36 | return ( 37 |
38 |

Merge into another account

39 | 40 | 41 | 42 | {destination && ( 43 | 44 | )} 45 | 46 | {tx && } 47 |
48 | ); 49 | } 50 | } 51 | 52 | export default MergeTransaction; 53 | -------------------------------------------------------------------------------- /playground/src/components/Offers.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | class Offers extends Component { 4 | state = { 5 | offers: null, 6 | err: null, 7 | updateTimes: [], 8 | streamEnder: null, 9 | }; 10 | 11 | componentDidMount() { 12 | if (this.props.dataProvider) { 13 | this._fetchOffers(this.props.dataProvider); 14 | } 15 | } 16 | 17 | componentWillUpdate(nextProps) { 18 | if ( 19 | typeof this.props.dataProvider !== typeof nextProps.dataProvider || 20 | this.props.dataProvider.getAccountKey() !== 21 | nextProps.dataProvider.getAccountKey() 22 | ) { 23 | this._fetchOffers(nextProps.dataProvider); 24 | } 25 | } 26 | 27 | componentWillUnmount() { 28 | if (this.state.streamEnder) { 29 | this.state.streamEnder(); 30 | } 31 | } 32 | 33 | _fetchOffers = async (dataProvider) => { 34 | // if there was a previous data provider, kill the 35 | if (this.state.streamEnder) { 36 | this.state.streamEnder(); 37 | } 38 | 39 | this.setState({ 40 | dataProvider, 41 | offers: null, 42 | err: null, 43 | updateTimes: [], 44 | streamEnder: null, 45 | }); 46 | 47 | try { 48 | const offers = await dataProvider.fetchOpenOffers(); 49 | 50 | this.setState({ offers }); 51 | } catch (e) { 52 | this.setState({ err: e.toString() }); 53 | } 54 | }; 55 | 56 | render() { 57 | const { offers, err, updateTimes } = this.state; 58 | return ( 59 |
60 |

Offers

61 | 62 |
    63 | {updateTimes.map((time) => ( 64 |
  • {time.toString()}
  • 65 | ))} 66 |
67 | 68 | {offers &&
{JSON.stringify(offers, null, 2)}
} 69 | {err &&

Error: {err.toString()}

} 70 |
71 | ); 72 | } 73 | } 74 | 75 | export default Offers; 76 | -------------------------------------------------------------------------------- /playground/src/components/Payments.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import moment from "moment"; 3 | 4 | class Payments extends Component { 5 | state = { 6 | payments: [], 7 | err: null, 8 | streamEnder: null, 9 | }; 10 | 11 | componentDidMount() { 12 | if (this.props.dataProvider) { 13 | this._watchPayments(this.props.dataProvider); 14 | } 15 | } 16 | 17 | componentWillUpdate(nextProps) { 18 | if ( 19 | typeof this.props.dataProvider !== typeof nextProps.dataProvider || 20 | this.props.dataProvider.getAccountKey() !== 21 | nextProps.dataProvider.getAccountKey() 22 | ) { 23 | this._watchPayments(nextProps.dataProvider); 24 | } 25 | } 26 | 27 | componentWillUnmount() { 28 | if (this.state.streamEnder) { 29 | this.state.streamEnder(); 30 | } 31 | } 32 | 33 | _watchPayments = async (dataProvider) => { 34 | // if there was a previous data provider, kill the 35 | if (this.state.streamEnder) { 36 | this.state.streamEnder(); 37 | } 38 | 39 | this.setState({ 40 | dataProvider, 41 | payments: [], 42 | err: null, 43 | streamEnder: null, 44 | }); 45 | 46 | const streamEnder = dataProvider.watchPayments({ 47 | onMessage: (payment) => { 48 | this.setState({ 49 | payments: [ 50 | { payment, updateTime: new Date() }, 51 | ...this.state.payments, 52 | ], 53 | }); 54 | }, 55 | onError: (err) => { 56 | console.log("error: ", err); 57 | this.setState({ err }); 58 | streamEnder(); 59 | }, 60 | }); 61 | 62 | this.setState({ 63 | streamEnder, 64 | }); 65 | }; 66 | 67 | render() { 68 | const { payments, err } = this.state; 69 | return ( 70 |
71 |

Payments

72 |
    73 | {payments 74 | .sort((a, b) => b.payment.timestamp - a.payment.timestamp) 75 | .map(({ payment, updateTime }) => ( 76 |
  • 77 | Updated: {updateTime.toString()} 78 |
    79 |
      80 |
    • {moment.unix(payment.timestamp).format("LLL")}
    • 81 | {payment.isInitialFunding &&
    • First funding
    • } 82 |
    • 83 | {payment.isRecipient ? "Received" : "Sent"}{" "} 84 | {payment.amount.toString()} {payment.token.code} 85 |
    • 86 |
    • 87 | {payment.isRecipient ? "From" : "To"}{" "} 88 | {payment.otherAccount.publicKey} 89 |
    • 90 |
    • Memo: {payment.memo}
    • 91 |
    • Memo type: {payment.memoType}
    • 92 |
    93 | {/* */} 94 |
  • 95 | ))} 96 |
97 | 98 | {err &&

Error: {err.toString()}

} 99 |
100 | ); 101 | } 102 | } 103 | 104 | export default Payments; 105 | -------------------------------------------------------------------------------- /playground/src/components/Trades.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | class Trades extends Component { 4 | state = { 5 | offers: null, 6 | err: null, 7 | updateTimes: [], 8 | streamEnder: null, 9 | }; 10 | 11 | componentDidMount() { 12 | if (this.props.dataProvider) { 13 | this._fetchTrades(this.props.dataProvider); 14 | } 15 | } 16 | 17 | componentWillUpdate(nextProps) { 18 | if ( 19 | typeof this.props.dataProvider !== typeof nextProps.dataProvider || 20 | this.props.dataProvider.getAccountKey() !== 21 | nextProps.dataProvider.getAccountKey() 22 | ) { 23 | this._fetchTrades(nextProps.dataProvider); 24 | } 25 | } 26 | 27 | componentWillUnmount() { 28 | if (this.state.streamEnder) { 29 | this.state.streamEnder(); 30 | } 31 | } 32 | 33 | _fetchTrades = async (dataProvider) => { 34 | // if there was a previous data provider, kill the 35 | if (this.state.streamEnder) { 36 | this.state.streamEnder(); 37 | } 38 | 39 | this.setState({ 40 | dataProvider, 41 | offers: null, 42 | err: null, 43 | updateTimes: [], 44 | streamEnder: null, 45 | }); 46 | 47 | try { 48 | const offers = await dataProvider.fetchTrades(); 49 | 50 | this.setState({ offers }); 51 | } catch (e) { 52 | this.setState({ err: e.toString() }); 53 | } 54 | }; 55 | 56 | render() { 57 | const { offers, err, updateTimes } = this.state; 58 | return ( 59 |
60 |

Trades

61 | 62 |
    63 | {updateTimes.map((time) => ( 64 |
  • {time.toString()}
  • 65 | ))} 66 |
67 | 68 | {offers &&
{JSON.stringify(offers, null, 2)}
} 69 | {err &&

Error: {err.toString()}

} 70 |
71 | ); 72 | } 73 | } 74 | 75 | export default Trades; 76 | -------------------------------------------------------------------------------- /playground/src/components/TransactionViewer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import styled from "styled-components"; 4 | 5 | import OperationViewer from "./OperationViewer"; 6 | 7 | export function getXDRFromTransaction(transaction) { 8 | const xdrBuffer = transaction.toEnvelope().toXDR(); 9 | return xdrBuffer.toString("base64"); 10 | } 11 | 12 | const El = styled.div``; 13 | 14 | const PreEl = styled.div` 15 | max-height: 300px; 16 | overflow: auto; 17 | `; 18 | 19 | const PreHeaderEl = styled.div` 20 | padding: 10px 20px; 21 | `; 22 | 23 | const PreSubheaderEl = styled.div` 24 | padding: 10px 20px; 25 | font-size: 12px; 26 | line-height: 20px; 27 | `; 28 | 29 | const PreBodyEl = styled.div` 30 | padding: 0 20px; 31 | `; 32 | 33 | const DetailsEl = styled.div` 34 | font-size: 12px; 35 | display: flex; 36 | justify-content: space-between; 37 | align-items: center; 38 | `; 39 | 40 | const TransactionViewer = ({ transaction }) => { 41 | const { operations, memo } = transaction; 42 | 43 | const xdr = getXDRFromTransaction(transaction); 44 | 45 | const hasMemo = memo && memo._type && memo._value; 46 | 47 | const memoValue = 48 | // If it's binary, render as hex. Create a new array because otherwise it's 49 | // still a Uint8Array, which doesn't like holding string values. 50 | memo._value instanceof Uint8Array 51 | ? [...memo._value].map((x) => `0${x.toString(16)}`.substr(-2)).join("") 52 | : memo._value; 53 | 54 | return ( 55 | 56 | 57 | 58 |

{operations.length} operation(s)

59 |
60 | 61 | {operations.map((operation, i) => ( 62 | // eslint-disable-next-line react/no-array-index-key 63 | 64 | ))} 65 | 66 | 67 | {!hasMemo && ( 68 | 69 | <>No memo attached! 70 | 71 | )} 72 | {hasMemo && `Memo (${memo._type}): ${memoValue}`} 73 | 74 |
75 | 76 | 77 |

Hash: {transaction.hash().toString("hex")}

78 | 79 | 86 | <>View in Stellar Laboratory 87 | 88 |
89 |
90 | ); 91 | }; 92 | 93 | TransactionViewer.propTypes = { 94 | transaction: PropTypes.shape({ 95 | operations: PropTypes.arrayOf(PropTypes.object).isRequired, 96 | signatures: PropTypes.arrayOf(PropTypes.object).isRequired, 97 | source: PropTypes.string.isRequired, 98 | fee: PropTypes.number.isRequired, 99 | sequence: PropTypes.string.isRequired, 100 | memo: PropTypes.object, 101 | 102 | // functions 103 | hash: PropTypes.func, 104 | toEnvelope: PropTypes.func, 105 | }).isRequired, 106 | }; 107 | 108 | export default TransactionViewer; 109 | -------------------------------------------------------------------------------- /playground/src/components/TransferProvider.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import Json from "react-json-view"; 4 | import * as WalletSdk from "@stellar/wallet-sdk"; 5 | import { 6 | Input, 7 | Button, 8 | ButtonThemes, 9 | Checkbox, 10 | Select, 11 | } from "@stellar/elements"; 12 | 13 | class TransferProvider extends Component { 14 | state = { 15 | depositProvider: null, 16 | url: "", 17 | isTestnet: false, 18 | assetCode: "", 19 | 20 | transactions: null, 21 | transactionError: null, 22 | info: null, 23 | }; 24 | 25 | componentDidMount() { 26 | const localUrl = localStorage.getItem("transferUrl"); 27 | if (localUrl) { 28 | this.setState({ url: localUrl }); 29 | } 30 | } 31 | 32 | _setUrl = (url) => { 33 | const depositProvider = new WalletSdk.DepositProvider( 34 | url, 35 | this.props.dataProvider.getAccountKey(), 36 | ); 37 | 38 | this.setState({ 39 | depositProvider, 40 | }); 41 | 42 | localStorage.setItem("transferUrl", url); 43 | 44 | depositProvider.fetchInfo().then((info) => this.setState({ info })); 45 | }; 46 | 47 | render() { 48 | const { 49 | depositProvider, 50 | url, 51 | info, 52 | isTestnet, 53 | assetCode, 54 | transactions, 55 | transactionError, 56 | } = this.state; 57 | const { authToken } = this.props; 58 | 59 | return ( 60 |
61 |

Deposit provider

62 | 63 | {!depositProvider && ( 64 | <> 65 | 73 | this.setState({ isTestnet: !isTestnet })} 77 | /> 78 | 84 | 85 | )} 86 | 87 | {depositProvider && info && ( 88 | <> 89 |

Token Info

90 | 91 | 92 | 93 |

Transaction History

94 | 95 | 109 | 110 | {assetCode && !authToken && ( 111 |

Fetch an auth token above before continuing!

112 | )} 113 | 114 | {assetCode && authToken && ( 115 | 135 | )} 136 | 137 | {transactionError &&

Error: {transactionError}

} 138 | {transactions && } 139 | 140 | )} 141 |
142 | ); 143 | } 144 | } 145 | 146 | TransferProvider.propTypes = { 147 | dataProvider: PropTypes.object.isRequired, 148 | authToken: PropTypes.string, 149 | }; 150 | 151 | export default TransferProvider; 152 | -------------------------------------------------------------------------------- /playground/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | ReactDOM.render(, document.getElementById("root")); 6 | -------------------------------------------------------------------------------- /src/KeyManagerPlugins.ts: -------------------------------------------------------------------------------- 1 | import { BrowserStorageKeyStore } from "./plugins/BrowserStorageKeyStore"; 2 | import { IdentityEncrypter } from "./plugins/IdentityEncrypter"; 3 | import { LocalStorageKeyStore } from "./plugins/LocalStorageKeyStore"; 4 | import { MemoryKeyStore } from "./plugins/MemoryKeyStore"; 5 | import { ScryptEncrypter } from "./plugins/ScryptEncrypter"; 6 | 7 | export const KeyManagerPlugins: any = { 8 | BrowserStorageKeyStore, 9 | IdentityEncrypter, 10 | MemoryKeyStore, 11 | LocalStorageKeyStore, 12 | ScryptEncrypter, 13 | }; 14 | -------------------------------------------------------------------------------- /src/browser.ts: -------------------------------------------------------------------------------- 1 | import "regenerator-runtime/runtime"; 2 | export * from "./index"; 3 | -------------------------------------------------------------------------------- /src/constants/data.ts: -------------------------------------------------------------------------------- 1 | export enum EffectType { 2 | account_created = "account_created", 3 | account_credited = "account_credited", 4 | account_debited = "account_debited", 5 | account_home_domain_updated = "account_home_domain_updated", 6 | // tslint:disable-next-line max-line-length 7 | account_inflation_destination_updated = "account_inflation_destination_updated", 8 | account_removed = "account_removed", 9 | account_thresholds_updated = "account_thresholds_updated", 10 | signer_created = "signer_created", 11 | signer_removed = "signer_removed", 12 | signer_updated = "signer_updated", 13 | trade = "trade", 14 | trustline_created = "trustline_created", 15 | trustline_removed = "trustline_removed", 16 | trustline_updated = "trustline_updated", 17 | } 18 | -------------------------------------------------------------------------------- /src/constants/keys.ts: -------------------------------------------------------------------------------- 1 | export enum KeyType { 2 | albedo = "albedo", 3 | ledger = "ledger", 4 | freighter = "freighter", 5 | plaintextKey = "plaintextKey", 6 | trezor = "trezor", 7 | } 8 | -------------------------------------------------------------------------------- /src/constants/sep8.ts: -------------------------------------------------------------------------------- 1 | export enum ApprovalResponseStatus { 2 | success = "success", 3 | revised = "revised", 4 | pending = "pending", 5 | actionRequired = "action_required", 6 | rejected = "rejected", 7 | } 8 | 9 | export enum ActionResult { 10 | noFurtherActionRequired = "no_further_action_required", 11 | followNextUrl = "follow_next_url", 12 | } 13 | -------------------------------------------------------------------------------- /src/constants/stellar.ts: -------------------------------------------------------------------------------- 1 | export const BASE_RESERVE = 0.5; 2 | export const BASE_RESERVE_MIN_COUNT = 2; 3 | -------------------------------------------------------------------------------- /src/constants/transfers.ts: -------------------------------------------------------------------------------- 1 | export enum TransferResponseType { 2 | ok = "ok", 3 | non_interactive_customer_info_needed = "non_interactive_customer_info_needed", 4 | interactive_customer_info_needed = "interactive_customer_info_needed", 5 | customer_info_status = "customer_info_status", 6 | error = "error", 7 | } 8 | 9 | export enum TransactionStatus { 10 | incomplete = "incomplete", 11 | pending_user_transfer_start = "pending_user_transfer_start", 12 | pending_user_transfer_complete = "pending_user_transfer_complete", 13 | pending_external = "pending_external", 14 | pending_anchor = "pending_anchor", 15 | pending_stellar = "pending_stellar", 16 | pending_trust = "pending_trust", 17 | pending_user = "pending_user", 18 | completed = "completed", 19 | refunded = "refunded", 20 | no_market = "no_market", 21 | too_small = "too_small", 22 | too_large = "too_large", 23 | error = "error", 24 | } -------------------------------------------------------------------------------- /src/data/DataProvider.test.ts: -------------------------------------------------------------------------------- 1 | import { Networks } from "@stellar/stellar-sdk"; 2 | import { generatePlaintextKey } from "../fixtures/keys"; 3 | import { DataProvider } from "./DataProvider"; 4 | 5 | describe("Account validation", () => { 6 | test("works with null values", () => { 7 | try { 8 | const provider = new DataProvider({ 9 | // @ts-ignore 10 | accountOrKey: null, 11 | serverUrl: "https://horizon.stellar.org", 12 | networkPassphrase: Networks.PUBLIC, 13 | }); 14 | expect(provider).not.toBeInstanceOf(DataProvider); 15 | } catch (e) { 16 | expect(e).toBeTruthy(); 17 | expect((e as any).toString()).toBe("Error: No account key provided."); 18 | } 19 | }); 20 | 21 | test("works with real public keys", () => { 22 | try { 23 | const provider = new DataProvider({ 24 | accountOrKey: 25 | "GDZBHQFIHLVDF6GCRV5DT2STB6ZXAJR3JFGZNXNPLB35TH5GNMUVIAQP", 26 | serverUrl: "https://horizon.stellar.org", 27 | networkPassphrase: Networks.PUBLIC, 28 | }); 29 | expect(provider).toBeInstanceOf(DataProvider); 30 | } catch (e) { 31 | expect(e).toBeUndefined(); 32 | } 33 | }); 34 | 35 | test("works with real typed Keys", () => { 36 | try { 37 | const provider = new DataProvider({ 38 | accountOrKey: generatePlaintextKey(), 39 | serverUrl: "https://horizon.stellar.org", 40 | networkPassphrase: Networks.PUBLIC, 41 | }); 42 | expect(provider).toBeInstanceOf(DataProvider); 43 | } catch (e) { 44 | expect(e).toBeUndefined(); 45 | } 46 | }); 47 | 48 | test("Throw with bad key", () => { 49 | let provider; 50 | try { 51 | provider = new DataProvider({ 52 | accountOrKey: "I am not a stupid key you dumbdumb", 53 | serverUrl: "https://horizon.stellar.org", 54 | networkPassphrase: Networks.PUBLIC, 55 | }); 56 | } catch (e) { 57 | expect(e).toBeTruthy(); 58 | } 59 | expect(provider).not.toBeInstanceOf(DataProvider); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/data/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Asset } from "@stellar/stellar-sdk"; 2 | import { 3 | getBalanceIdentifier, 4 | getStellarSdkAsset, 5 | getTokenIdentifier, 6 | } from "./"; 7 | 8 | describe("getTokenIdentifier", () => { 9 | test("native element", () => { 10 | expect(getTokenIdentifier({ type: "native", code: "XLM" })).toEqual( 11 | "native", 12 | ); 13 | }); 14 | test("non-native element", () => { 15 | expect( 16 | getTokenIdentifier({ 17 | code: "BAT", 18 | type: "credit_alphanum4", 19 | issuer: { 20 | key: "GBDEVU63Y6NTHJQQZIKVTC23NWLQVP3WJ2RI2OTSJTNYOIGICST6DUXR", 21 | }, 22 | }), 23 | ).toEqual("BAT:GBDEVU63Y6NTHJQQZIKVTC23NWLQVP3WJ2RI2OTSJTNYOIGICST6DUXR"); 24 | }); 25 | }); 26 | 27 | describe("getBalanceIdentifier", () => { 28 | test("native balance", () => { 29 | expect( 30 | getBalanceIdentifier({ 31 | asset_type: "native", 32 | balance: "100", 33 | buying_liabilities: "foo", 34 | selling_liabilities: "bar", 35 | }), 36 | ).toEqual("native"); 37 | }); 38 | test("non-native balance", () => { 39 | expect( 40 | getBalanceIdentifier({ 41 | asset_code: "BAT", 42 | asset_issuer: 43 | "GBDEVU63Y6NTHJQQZIKVTC23NWLQVP3WJ2RI2OTSJTNYOIGICST6DUXR", 44 | asset_type: "credit_alphanum4", 45 | balance: "100", 46 | buying_liabilities: "foo", 47 | is_authorized: false, 48 | is_authorized_to_maintain_liabilities: false, 49 | is_clawback_enabled: false, 50 | last_modified_ledger: 1, 51 | limit: "foo", 52 | selling_liabilities: "bar", 53 | }), 54 | ).toEqual("BAT:GBDEVU63Y6NTHJQQZIKVTC23NWLQVP3WJ2RI2OTSJTNYOIGICST6DUXR"); 55 | }); 56 | test("liquidity pool share balance", () => { 57 | expect( 58 | getBalanceIdentifier({ 59 | asset_type: "liquidity_pool_shares", 60 | balance: "100", 61 | is_authorized: false, 62 | is_authorized_to_maintain_liabilities: false, 63 | is_clawback_enabled: false, 64 | last_modified_ledger: 1, 65 | limit: "foo", 66 | liquidity_pool_id: 67 | "0466a6bbafdc293b87f2ea7615919244057242b21ebf46b38d64536e8d2ac3c0", 68 | }), 69 | ).toEqual( 70 | "0466a6bbafdc293b87f2ea7615919244057242b21ebf46b38d64536e8d2ac3c0:lp", 71 | ); 72 | }); 73 | }); 74 | 75 | describe("getStellarSdkAsset", () => { 76 | test("native element", () => { 77 | expect(getStellarSdkAsset({ type: "native", code: "XLM" })).toEqual( 78 | Asset.native(), 79 | ); 80 | }); 81 | test("normal element", () => { 82 | expect( 83 | getStellarSdkAsset({ 84 | code: "BAT", 85 | type: "credit_alphanum4", 86 | issuer: { 87 | key: "GBDEVU63Y6NTHJQQZIKVTC23NWLQVP3WJ2RI2OTSJTNYOIGICST6DUXR", 88 | }, 89 | }), 90 | ).toEqual( 91 | new Asset( 92 | "BAT", 93 | "GBDEVU63Y6NTHJQQZIKVTC23NWLQVP3WJ2RI2OTSJTNYOIGICST6DUXR", 94 | ), 95 | ); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /src/data/index.ts: -------------------------------------------------------------------------------- 1 | import { Asset, Horizon } from "@stellar/stellar-sdk"; 2 | 3 | import { AssetToken, Token } from "../types"; 4 | 5 | /** 6 | * Get the string identifier for a token. 7 | * @returns "native" if the token is native, otherwise returns 8 | * `${tokenCode}:${issuerKey}`. 9 | */ 10 | export function getTokenIdentifier(token: Token): string { 11 | if (token.type === "native") { 12 | return "native"; 13 | } 14 | 15 | return `${token.code}:${(token as AssetToken).issuer.key}`; 16 | } 17 | 18 | /** 19 | * Get the string identifier for a balance line item from Horizon. The response 20 | * should be the same as if that balance was a Token object, and you passed it 21 | * through `getTokenIdentifier` 22 | * @returns Returns `${tokenCode}:${issuerKey}`. 23 | */ 24 | export function getBalanceIdentifier( 25 | balance: Horizon.HorizonApi.BalanceLine, 26 | ): string { 27 | if ("asset_issuer" in balance && !balance.asset_issuer) { 28 | return "native"; 29 | } 30 | switch (balance.asset_type) { 31 | case "credit_alphanum4": 32 | case "credit_alphanum12": 33 | return `${balance.asset_code}:${balance.asset_issuer}`; 34 | 35 | case "liquidity_pool_shares": 36 | return `${balance.liquidity_pool_id}:lp`; 37 | 38 | default: 39 | return "native"; 40 | } 41 | } 42 | 43 | /** 44 | * Convert a Wallet-SDK-formatted Token object to a Stellar SDK Asset object. 45 | * @returns Returns `${tokenCode}:${issuerKey}`. 46 | */ 47 | export function getStellarSdkAsset(token: Token): Asset { 48 | if (token.type === "native") { 49 | return Asset.native(); 50 | } 51 | 52 | return new Asset(token.code, (token as AssetToken).issuer.key); 53 | } 54 | -------------------------------------------------------------------------------- /src/data/makeDisplayableBalances.ts: -------------------------------------------------------------------------------- 1 | import { Horizon } from "@stellar/stellar-sdk"; 2 | import BigNumber from "bignumber.js"; 3 | 4 | import { BASE_RESERVE, BASE_RESERVE_MIN_COUNT } from "../constants/stellar"; 5 | import { BalanceMap } from "../types"; 6 | import { getBalanceIdentifier } from "./"; 7 | 8 | export function makeDisplayableBalances( 9 | accountDetails: Horizon.ServerApi.AccountRecord, 10 | ): BalanceMap { 11 | const { 12 | balances, 13 | subentry_count, 14 | num_sponsored, 15 | num_sponsoring, 16 | } = accountDetails; 17 | 18 | const displayableBalances = Object.values(balances).reduce( 19 | (memo, balance) => { 20 | const identifier = getBalanceIdentifier(balance); 21 | const total = new BigNumber(balance.balance); 22 | 23 | let sellingLiabilities = new BigNumber(0); 24 | let buyingLiabilities = new BigNumber(0); 25 | let available; 26 | 27 | if ("selling_liabilities" in balance) { 28 | sellingLiabilities = new BigNumber(balance.selling_liabilities); 29 | available = total.minus(sellingLiabilities); 30 | } 31 | 32 | if ("buying_liabilities" in balance) { 33 | buyingLiabilities = new BigNumber(balance.buying_liabilities); 34 | } 35 | 36 | if (identifier === "native") { 37 | // define the native balance line later 38 | return { 39 | ...memo, 40 | native: { 41 | token: { 42 | type: "native", 43 | code: "XLM", 44 | }, 45 | total, 46 | available, 47 | sellingLiabilities, 48 | buyingLiabilities, 49 | 50 | /* tslint:disable */ 51 | // https://developers.stellar.org/docs/glossary/sponsored-reserves/#sponsorship-effect-on-minimum-balance 52 | /* tslint:enable */ 53 | minimumBalance: new BigNumber(BASE_RESERVE_MIN_COUNT) 54 | .plus(subentry_count) 55 | .plus(num_sponsoring) 56 | .minus(num_sponsored) 57 | .times(BASE_RESERVE) 58 | .plus(sellingLiabilities), 59 | }, 60 | }; 61 | } 62 | 63 | /* tslint:disable */ 64 | const liquidityPoolBalance = balance as Horizon.HorizonApi.BalanceLineLiquidityPool; 65 | /* tslint:enable */ 66 | 67 | if (identifier.includes(":lp")) { 68 | return { 69 | ...memo, 70 | [identifier]: { 71 | liquidity_pool_id: liquidityPoolBalance.liquidity_pool_id, 72 | total, 73 | limit: new BigNumber(liquidityPoolBalance.limit), 74 | }, 75 | }; 76 | } 77 | 78 | const assetBalance = balance as Horizon.HorizonApi.BalanceLineAsset; 79 | const assetSponsor = assetBalance.sponsor 80 | ? { sponsor: assetBalance.sponsor } 81 | : {}; 82 | 83 | return { 84 | ...memo, 85 | [identifier]: { 86 | token: { 87 | type: assetBalance.asset_type, 88 | code: assetBalance.asset_code, 89 | issuer: { 90 | key: assetBalance.asset_issuer, 91 | }, 92 | }, 93 | sellingLiabilities, 94 | buyingLiabilities, 95 | total, 96 | limit: new BigNumber(assetBalance.limit), 97 | available: total.minus(sellingLiabilities), 98 | ...assetSponsor, 99 | }, 100 | }; 101 | }, 102 | {}, 103 | ); 104 | 105 | return displayableBalances as BalanceMap; 106 | } 107 | -------------------------------------------------------------------------------- /src/data/makeDisplayableOffers.test.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from "bignumber.js"; 2 | 3 | import { parseResponse } from "../testUtils"; 4 | 5 | import { OffersResponse } from "../fixtures/OffersResponse"; 6 | import { TradesResponsePartialFill } from "../fixtures/TradesResponse"; 7 | 8 | import { makeDisplayableOffers } from "./makeDisplayableOffers"; 9 | 10 | it("makes offers from partial fill", () => { 11 | const tradeResponse = parseResponse(TradesResponsePartialFill); 12 | const offers = makeDisplayableOffers( 13 | { publicKey: "PHYREXIA" }, 14 | { 15 | // @ts-ignore 16 | offers: parseResponse(OffersResponse).records, 17 | // @ts-ignore 18 | tradeResponses: [tradeResponse.records], 19 | }, 20 | ); 21 | 22 | expect(offers[0]).toEqual({ 23 | id: "76884793", 24 | offerer: { 25 | publicKey: "PHYREXIA", 26 | }, 27 | paymentToken: { 28 | type: "native", 29 | code: "XLM", 30 | }, 31 | incomingToken: { 32 | type: "credit_alphanum4", 33 | code: "USD", 34 | issuer: { 35 | key: "GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX", 36 | }, 37 | }, 38 | incomingAmount: new BigNumber(2), 39 | paymentAmount: new BigNumber(8), 40 | incomingTokenPrice: new BigNumber(4), 41 | initialPaymentAmount: new BigNumber(8), 42 | timestamp: 1553635587, 43 | resultingTrades: [], 44 | }); 45 | expect(offers[1].resultingTrades).toEqual(["99777639383887873-0"]); 46 | expect(offers[1].initialPaymentAmount).toEqual(new BigNumber("520.61832")); 47 | }); 48 | -------------------------------------------------------------------------------- /src/data/makeDisplayableOffers.ts: -------------------------------------------------------------------------------- 1 | import { AssetType } from "@stellar/stellar-sdk"; 2 | import { Horizon } from "@stellar/stellar-sdk"; 3 | import BigNumber from "bignumber.js"; 4 | import flatten from "lodash/flatten"; 5 | 6 | import { makeDisplayableTrades } from "./makeDisplayableTrades"; 7 | 8 | import { Account, Offer, Token, Trade } from "../types"; 9 | 10 | export type TradeCollection = Horizon.ServerApi.TradeRecord[]; 11 | 12 | export interface DisplayableOffersParams { 13 | offers: Horizon.ServerApi.OfferRecord[]; 14 | tradeResponses: TradeCollection[]; 15 | } 16 | 17 | interface OfferIdMap { 18 | [offerid: string]: Trade[]; 19 | } 20 | 21 | export function makeDisplayableOffers( 22 | subjectAccount: Account, 23 | params: DisplayableOffersParams, 24 | ): Offer[] { 25 | const { offers, tradeResponses } = params; 26 | const trades = flatten(tradeResponses); 27 | 28 | // make a map of offerids to the trades involved with them 29 | // (reminder that each trade has two offerids, one for each side) 30 | const offeridsToTradesMap: OfferIdMap = makeDisplayableTrades( 31 | subjectAccount, 32 | trades, 33 | ).reduce((memo: any, trade: Trade) => { 34 | if (trade.paymentOfferId) { 35 | memo[trade.paymentOfferId] = [ 36 | ...(memo[trade.paymentOfferId] || []), 37 | trade, 38 | ]; 39 | } 40 | 41 | return memo; 42 | }, {}); 43 | 44 | return offers.map( 45 | (offer: Horizon.ServerApi.OfferRecord): Offer => { 46 | const { 47 | id, 48 | selling, 49 | seller, 50 | buying, 51 | amount, 52 | price_r, 53 | last_modified_time, 54 | } = offer; 55 | 56 | const paymentToken: Token = { 57 | type: selling.asset_type as AssetType, 58 | code: (selling.asset_code as string) || "XLM", 59 | issuer: 60 | selling.asset_type === "native" 61 | ? undefined 62 | : { 63 | key: selling.asset_issuer as string, 64 | }, 65 | }; 66 | 67 | const incomingToken: Token = { 68 | type: buying.asset_type as AssetType, 69 | code: (buying.asset_code as string) || "XLM", 70 | issuer: 71 | buying.asset_type === "native" 72 | ? undefined 73 | : { 74 | key: buying.asset_issuer as string, 75 | }, 76 | }; 77 | 78 | const tradePaymentAmount: BigNumber = ( 79 | offeridsToTradesMap[id] || [] 80 | ).reduce((memo: BigNumber, trade: Trade): BigNumber => { 81 | return memo.plus(trade.paymentAmount); 82 | }, new BigNumber(0)); 83 | 84 | return { 85 | id: `${id}`, 86 | offerer: { 87 | publicKey: seller as string, 88 | }, 89 | timestamp: Math.floor(new Date(last_modified_time).getTime() / 1000), 90 | paymentToken, 91 | paymentAmount: new BigNumber(amount), 92 | initialPaymentAmount: new BigNumber(amount).plus(tradePaymentAmount), 93 | incomingToken, 94 | incomingAmount: new BigNumber(price_r.n).div(price_r.d).times(amount), 95 | incomingTokenPrice: new BigNumber(1).div(price_r.n).times(price_r.d), 96 | resultingTrades: (offeridsToTradesMap[id] || []).map( 97 | (trade) => trade.id, 98 | ), 99 | }; 100 | }, 101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /src/data/makeDisplayablePayments.test.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from "bignumber.js"; 2 | 3 | import { Payments } from "../fixtures/PaymentsResponse"; 4 | import { parseResponse } from "../testUtils"; 5 | import { makeDisplayablePayments } from "./makeDisplayablePayments"; 6 | 7 | it("Makes payments", async () => { 8 | const paymentResponse = parseResponse(Payments); 9 | const payments = await makeDisplayablePayments( 10 | { publicKey: "PHYREXIA" }, 11 | paymentResponse.records, 12 | ); 13 | 14 | expect(payments[0].id).toEqual("74305992237531137"); 15 | expect(payments[0].amount).toEqual(new BigNumber("1000")); 16 | expect(payments[0].isInitialFunding).toEqual(true); 17 | expect(payments[0].isRecipient).toEqual(true); 18 | expect(payments[0].otherAccount.publicKey).toEqual("SERRA"); 19 | 20 | expect(payments[1].id).toEqual("74961434311663617"); 21 | expect(payments[1].amount).toEqual(new BigNumber("10")); 22 | expect(payments[1].token.code).toEqual("XLM"); 23 | expect(payments[1].sourceToken).toEqual({ code: "XLM", type: "native" }); 24 | expect(payments[1].isInitialFunding).toEqual(false); 25 | expect(payments[1].isRecipient).toEqual(true); 26 | expect(payments[1].otherAccount.publicKey).toEqual("SERRA"); 27 | 28 | expect(payments[3].id).toEqual("95518827122688002"); 29 | expect(payments[3].amount).toEqual(new BigNumber("0.0000300")); 30 | expect(payments[3].token.code).toEqual("XLM"); 31 | expect(payments[3].isInitialFunding).toEqual(false); 32 | expect(payments[3].isRecipient).toEqual(true); 33 | expect(payments[3].otherAccount.publicKey).toEqual( 34 | "GDM4UWTGHCWSTM7Z46PNF4BLH35GS6IUZYBWNNI4VU5KVIHYSIVQ55Y6", 35 | ); 36 | }); 37 | -------------------------------------------------------------------------------- /src/data/makeDisplayableTrades.test.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from "bignumber.js"; 2 | 3 | import { parseResponse } from "../testUtils"; 4 | 5 | import { TradesResponsePartialFill } from "../fixtures/TradesResponse"; 6 | 7 | import { makeDisplayableTrades } from "./makeDisplayableTrades"; 8 | 9 | it("makes trades from real-world examples", () => { 10 | const trades = makeDisplayableTrades( 11 | { publicKey: "PHYREXIA" }, 12 | // @ts-ignore 13 | parseResponse(TradesResponsePartialFill).records, 14 | ); 15 | 16 | expect(trades).toEqual([ 17 | { 18 | id: "99777639383887873-0", 19 | 20 | paymentToken: { 21 | code: "XLM", 22 | type: "native", 23 | }, 24 | paymentAmount: new BigNumber("363.0644948"), 25 | paymentOfferId: "78448448", 26 | 27 | incomingToken: { 28 | code: "BAT", 29 | type: "credit_alphanum4", 30 | issuer: { 31 | key: "GBDEVU63Y6NTHJQQZIKVTC23NWLQVP3WJ2RI2OTSJTNYOIGICST6DUXR", 32 | }, 33 | }, 34 | incomingAmount: new BigNumber("139.5761839"), 35 | incomingAccount: { 36 | publicKey: "SERRA", 37 | }, 38 | incomingOfferId: "78448401", 39 | 40 | timestamp: 1554236520, 41 | }, 42 | ]); 43 | }); 44 | -------------------------------------------------------------------------------- /src/data/makeDisplayableTrades.ts: -------------------------------------------------------------------------------- 1 | import { AssetType, Horizon } from "@stellar/stellar-sdk"; 2 | import BigNumber from "bignumber.js"; 3 | 4 | import { Account, Token, Trade } from "../types"; 5 | 6 | /* 7 | { 8 | _links: { 9 | self: { 10 | href: "", 11 | }, 12 | base: { 13 | href: "https://horizon.stellar.org/accounts/PHYREXIA", 14 | }, 15 | counter: { 16 | href: "https://horizon.stellar.org/accounts/SERRA", 17 | }, 18 | operation: { 19 | href: "https://horizon.stellar.org/operations/99777639383887873", 20 | }, 21 | }, 22 | id: "99777639383887873-0", 23 | paging_token: "99777639383887873-0", 24 | ledger_close_time: "2019-04-02T20:22:00Z", 25 | offer_id: "78448401", 26 | base_offer_id: "78448448", 27 | base_account: "PHYREXIA", 28 | base_amount: "363.0644948", 29 | base_asset_type: "native", 30 | counter_offer_id: "78448401", 31 | counter_account: "SERRA", 32 | counter_amount: "139.5761839", 33 | counter_asset_type: "credit_alphanum4", 34 | counter_asset_code: "BAT", 35 | counter_asset_issuer: 36 | "GBDEVU63Y6NTHJQQZIKVTC23NWLQVP3WJ2RI2OTSJTNYOIGICST6DUXR", 37 | base_is_seller: false, 38 | price: { 39 | n: 10000000, 40 | d: 26011923, 41 | }, 42 | }, 43 | 44 | */ 45 | 46 | export function makeDisplayableTrades( 47 | subjectAccount: Account, 48 | trades: Horizon.ServerApi.TradeRecord[], 49 | ): Trade[] { 50 | // make a map of trades to their original offerids 51 | return trades.map( 52 | (trade: Horizon.ServerApi.TradeRecord): Trade => { 53 | const base = { 54 | publicKey: trade.base_account || "", 55 | }; 56 | 57 | const counter = { 58 | publicKey: trade.counter_account || "", 59 | }; 60 | 61 | const isSubjectBase: boolean = 62 | base.publicKey === subjectAccount.publicKey; 63 | 64 | const baseToken: Token = { 65 | type: trade.base_asset_type as AssetType, 66 | code: (trade.base_asset_code as string) || "XLM", 67 | issuer: 68 | trade.base_asset_type === "native" 69 | ? undefined 70 | : { 71 | key: trade.base_asset_issuer as string, 72 | }, 73 | }; 74 | 75 | const counterToken: Token = { 76 | type: trade.counter_asset_type as AssetType, 77 | code: (trade.counter_asset_code as string) || "XLM", 78 | issuer: 79 | trade.counter_asset_type === "native" 80 | ? undefined 81 | : { 82 | key: trade.counter_asset_issuer as string, 83 | }, 84 | }; 85 | 86 | let paymentOfferId; 87 | let incomingOfferId; 88 | 89 | if ("base_offer_id" in trade) { 90 | paymentOfferId = isSubjectBase 91 | ? trade.base_offer_id 92 | : trade.counter_offer_id; 93 | 94 | incomingOfferId = isSubjectBase 95 | ? trade.counter_offer_id 96 | : trade.base_offer_id; 97 | } 98 | 99 | return { 100 | id: trade.id, 101 | timestamp: Math.floor( 102 | new Date(trade.ledger_close_time).getTime() / 1000, 103 | ), 104 | 105 | paymentToken: isSubjectBase ? baseToken : counterToken, 106 | paymentAmount: isSubjectBase 107 | ? new BigNumber(trade.base_amount) 108 | : new BigNumber(trade.counter_amount), 109 | paymentOfferId, 110 | 111 | incomingToken: isSubjectBase ? counterToken : baseToken, 112 | incomingAmount: isSubjectBase 113 | ? new BigNumber(trade.counter_amount) 114 | : new BigNumber(trade.base_amount), 115 | incomingAccount: isSubjectBase ? counter : base, 116 | incomingOfferId, 117 | }; 118 | }, 119 | ); 120 | } 121 | -------------------------------------------------------------------------------- /src/fixtures/AccountResponse.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable max-line-length 2 | 3 | export const AccountResponse = { 4 | _links: { 5 | self: { 6 | href: "https://horizon.stellar.org/accounts/PHYREXIA", 7 | }, 8 | transactions: { 9 | href: 10 | "https://horizon.stellar.org/accounts/PHYREXIA/transactions{?cursor,limit,order}", 11 | templated: true, 12 | }, 13 | operations: { 14 | href: 15 | "https://horizon.stellar.org/accounts/PHYREXIA/operations{?cursor,limit,order}", 16 | templated: true, 17 | }, 18 | payments: { 19 | href: 20 | "https://horizon.stellar.org/accounts/PHYREXIA/payments{?cursor,limit,order}", 21 | templated: true, 22 | }, 23 | effects: { 24 | href: 25 | "https://horizon.stellar.org/accounts/PHYREXIA/effects{?cursor,limit,order}", 26 | templated: true, 27 | }, 28 | offers: { 29 | href: 30 | "https://horizon.stellar.org/accounts/PHYREXIA/offers{?cursor,limit,order}", 31 | templated: true, 32 | }, 33 | trades: { 34 | href: 35 | "https://horizon.stellar.org/accounts/PHYREXIA/trades{?cursor,limit,order}", 36 | templated: true, 37 | }, 38 | data: { 39 | href: "https://horizon.stellar.org/accounts/PHYREXIA/data/{key}", 40 | templated: true, 41 | }, 42 | }, 43 | id: "PHYREXIA", 44 | paging_token: "", 45 | account_id: "PHYREXIA", 46 | sequence: "74305992237514793", 47 | subentry_count: 5, 48 | num_sponsored: 0, 49 | num_sponsoring: 0, 50 | inflation_destination: 51 | "GCCD6AJOYZCUAQLX32ZJF2MKFFAUJ53PVCFQI3RHWKL3V47QYE2BNAUT", 52 | last_modified_ledger: 22997383, 53 | thresholds: { 54 | low_threshold: 0, 55 | med_threshold: 0, 56 | high_threshold: 0, 57 | }, 58 | flags: { 59 | auth_required: false, 60 | auth_revocable: false, 61 | auth_immutable: false, 62 | }, 63 | balances: [ 64 | { 65 | balance: "0.0000000", 66 | limit: "922337203685.4775807", 67 | buying_liabilities: "0.0000000", 68 | selling_liabilities: "0.0000000", 69 | last_modified_ledger: 22897214, 70 | asset_type: "credit_alphanum4", 71 | asset_code: "BAT", 72 | asset_issuer: "GBDEVU63Y6NTHJQQZIKVTC23NWLQVP3WJ2RI2OTSJTNYOIGICST6DUXR", 73 | }, 74 | { 75 | balance: "19.0000000", 76 | limit: "922337203685.4775807", 77 | buying_liabilities: "0.0000000", 78 | selling_liabilities: "0.0000000", 79 | last_modified_ledger: 19145605, 80 | asset_type: "credit_alphanum4", 81 | asset_code: "REPO", 82 | asset_issuer: "GCZNF24HPMYTV6NOEHI7Q5RJFFUI23JKUKY3H3XTQAFBQIBOHD5OXG3B", 83 | }, 84 | { 85 | balance: "10.0000000", 86 | limit: "922337203685.4775807", 87 | buying_liabilities: "0.0000000", 88 | selling_liabilities: "0.0000000", 89 | last_modified_ledger: 18902253, 90 | asset_type: "credit_alphanum4", 91 | asset_code: "TERN", 92 | asset_issuer: "GDGQDVO6XPFSY4NMX75A7AOVYCF5JYGW2SHCJJNWCQWIDGOZB53DGP6C", 93 | }, 94 | { 95 | balance: "0.0000000", 96 | limit: "922337203685.4775807", 97 | buying_liabilities: "0.0000000", 98 | selling_liabilities: "0.0000000", 99 | last_modified_ledger: 22718536, 100 | asset_type: "credit_alphanum4", 101 | asset_code: "WSD", 102 | asset_issuer: "GDSVWEA7XV6M5XNLODVTPCGMAJTNBLZBXOFNQD3BNPNYALEYBNT6CE2V", 103 | }, 104 | { 105 | balance: "1.0000000", 106 | limit: "922337203685.4775807", 107 | buying_liabilities: "0.0000000", 108 | selling_liabilities: "0.0000000", 109 | last_modified_ledger: 22721290, 110 | asset_type: "credit_alphanum4", 111 | asset_code: "USD", 112 | asset_issuer: "GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX", 113 | }, 114 | { 115 | balance: "1.0000000", 116 | limit: "922337203685.4775807", 117 | last_modified_ledger: 22721290, 118 | liquidity_pool_id: 119 | "0466a6bbafdc293b87f2ea7615919244057242b21ebf46b38d64536e8d2ac3c0", 120 | asset_type: "liquidity_pool_shares", 121 | asset_code: "USD", 122 | asset_issuer: "GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX", 123 | }, 124 | { 125 | balance: "999.5689234", 126 | buying_liabilities: "0.0000000", 127 | selling_liabilities: "0.0000000", 128 | asset_type: "native", 129 | }, 130 | ], 131 | signers: [ 132 | { 133 | weight: 1, 134 | key: "PHYREXIA", 135 | type: "ed25519_public_key", 136 | }, 137 | ], 138 | data: {}, 139 | }; 140 | -------------------------------------------------------------------------------- /src/fixtures/OffersResponse.ts: -------------------------------------------------------------------------------- 1 | export const OffersResponse = { 2 | _links: { 3 | self: { 4 | href: 5 | "https://horizon.stellar.org/accounts/PHYREXIA/offers?" + 6 | "c=0&cursor=&limit=50&order=asc", 7 | }, 8 | next: { 9 | href: 10 | "https://horizon.stellar.org/accounts/PHYREXIA/offers?" + 11 | "c=0&cursor=76884793&limit=50&order=asc", 12 | }, 13 | prev: { 14 | href: 15 | "https://horizon.stellar.org/accounts/PHYREXIA/offers?" + 16 | "c=0&cursor=76884793&limit=50&order=desc", 17 | }, 18 | }, 19 | _embedded: { 20 | records: [ 21 | { 22 | _links: { 23 | self: { 24 | href: "https://horizon.stellar.org/offers/76884793", 25 | }, 26 | offer_maker: { 27 | href: "https://horizon.stellar.org/accounts/PHYREXIA", 28 | }, 29 | }, 30 | id: 76884793, 31 | paging_token: "76884793", 32 | seller: "PHYREXIA", 33 | selling: { 34 | asset_type: "native", 35 | }, 36 | buying: { 37 | asset_type: "credit_alphanum4", 38 | asset_code: "USD", 39 | asset_issuer: 40 | "GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX", 41 | }, 42 | amount: "8.0000000", 43 | price_r: { 44 | n: 1, 45 | d: 4, 46 | }, 47 | price: "0.2500000", 48 | last_modified_ledger: 23121355, 49 | last_modified_time: "2019-03-26T21:26:27Z", 50 | }, 51 | { 52 | _links: { 53 | self: { 54 | href: "https://horizon.stellar.org/offers/78448448", 55 | }, 56 | offer_maker: { 57 | href: "https://horizon.stellar.org/accounts/PHYREXIA", 58 | }, 59 | }, 60 | id: 78448448, 61 | paging_token: "78448448", 62 | seller: "PHYREXIA", 63 | selling: { 64 | asset_type: "native", 65 | }, 66 | buying: { 67 | asset_type: "credit_alphanum4", 68 | asset_code: "BAT", 69 | asset_issuer: 70 | "GBDEVU63Y6NTHJQQZIKVTC23NWLQVP3WJ2RI2OTSJTNYOIGICST6DUXR", 71 | }, 72 | amount: "157.5538252", 73 | price_r: { 74 | n: 2500000, 75 | d: 6507729, 76 | }, 77 | price: "0.3841586", 78 | last_modified_ledger: 23231292, 79 | last_modified_time: "2019-04-02T20:22:00Z", 80 | }, 81 | ], 82 | }, 83 | }; 84 | -------------------------------------------------------------------------------- /src/fixtures/TradesResponse.ts: -------------------------------------------------------------------------------- 1 | export const TradesResponsePartialFill = { 2 | _links: { 3 | self: { 4 | href: 5 | "https://horizon.stellar.org/accounts/PHYREXIA/trades?cursor=" + 6 | "\u0026limit=10\u0026order=desc", 7 | }, 8 | next: { 9 | href: 10 | "https://horizon.stellar.org/accounts/PHYREXIA/trades?cursor=" + 11 | "81184558455754753-22\u0026limit=10\u0026order=desc", 12 | }, 13 | prev: { 14 | href: 15 | "https://horizon.stellar.org/accounts/PHYREXIA/trades?cursor=" + 16 | "99777678038634508-0\u0026limit=10\u0026order=asc", 17 | }, 18 | }, 19 | _embedded: { 20 | records: [ 21 | // the partial trade 22 | { 23 | _links: { 24 | self: { 25 | href: "", 26 | }, 27 | base: { 28 | href: "https://horizon.stellar.org/accounts/PHYREXIA", 29 | }, 30 | counter: { 31 | href: "https://horizon.stellar.org/accounts/SERRA", 32 | }, 33 | operation: { 34 | href: "https://horizon.stellar.org/operations/99777639383887873", 35 | }, 36 | }, 37 | id: "99777639383887873-0", 38 | paging_token: "99777639383887873-0", 39 | ledger_close_time: "2019-04-02T20:22:00Z", 40 | offer_id: "78448401", 41 | base_offer_id: "78448448", 42 | base_account: "PHYREXIA", 43 | base_amount: "363.0644948", 44 | base_asset_type: "native", 45 | counter_offer_id: "78448401", 46 | counter_account: "SERRA", 47 | counter_amount: "139.5761839", 48 | counter_asset_type: "credit_alphanum4", 49 | counter_asset_code: "BAT", 50 | counter_asset_issuer: 51 | "GBDEVU63Y6NTHJQQZIKVTC23NWLQVP3WJ2RI2OTSJTNYOIGICST6DUXR", 52 | base_is_seller: false, 53 | price: { 54 | n: 10000000, 55 | d: 26011923, 56 | }, 57 | }, 58 | ], 59 | }, 60 | }; 61 | 62 | export const TradesResponseFullFill = { 63 | _links: { 64 | self: { 65 | href: 66 | "https://horizon.stellar.org/accounts/PHYREXIA/trades?cursor=" + 67 | "\u0026limit=10\u0026order=desc", 68 | }, 69 | next: { 70 | href: 71 | "https://horizon.stellar.org/accounts/PHYREXIA/trades?cursor=" + 72 | "81184558455754753-22\u0026limit=10\u0026order=desc", 73 | }, 74 | prev: { 75 | href: 76 | "https://horizon.stellar.org/accounts/PHYREXIA/trades?cursor=" + 77 | "99777678038634508-0\u0026limit=10\u0026order=asc", 78 | }, 79 | }, 80 | _embedded: { 81 | records: [ 82 | // the closing trade 83 | { 84 | _links: { 85 | self: { 86 | href: "", 87 | }, 88 | base: { 89 | href: "https://horizon.stellar.org/accounts/PHYREXIA", 90 | }, 91 | counter: { 92 | href: "https://horizon.stellar.org/accounts/SERRA", 93 | }, 94 | operation: { 95 | href: "https://horizon.stellar.org/operations/99777678038634508", 96 | }, 97 | }, 98 | id: "99777678038634508-0", 99 | paging_token: "99777678038634508-0", 100 | ledger_close_time: "2019-04-02T20:22:55Z", 101 | offer_id: "78448448", 102 | base_offer_id: "78448448", 103 | base_account: "PHYREXIA", 104 | base_amount: "157.5538252", 105 | base_asset_type: "native", 106 | counter_offer_id: "78448588", 107 | counter_account: "SERRA", 108 | counter_amount: "60.5256554", 109 | counter_asset_type: "credit_alphanum4", 110 | counter_asset_code: "BAT", 111 | counter_asset_issuer: 112 | "GBDEVU63Y6NTHJQQZIKVTC23NWLQVP3WJ2RI2OTSJTNYOIGICST6DUXR", 113 | base_is_seller: true, 114 | price: { 115 | n: 2500000, 116 | d: 6507729, 117 | }, 118 | }, 119 | // the partial trade 120 | { 121 | _links: { 122 | self: { 123 | href: "", 124 | }, 125 | base: { 126 | href: "https://horizon.stellar.org/accounts/PHYREXIA", 127 | }, 128 | counter: { 129 | href: "https://horizon.stellar.org/accounts/SERRA", 130 | }, 131 | operation: { 132 | href: "https://horizon.stellar.org/operations/99777639383887873", 133 | }, 134 | }, 135 | id: "99777639383887873-0", 136 | paging_token: "99777639383887873-0", 137 | ledger_close_time: "2019-04-02T20:22:00Z", 138 | offer_id: "78448401", 139 | base_offer_id: "78448448", 140 | base_account: "PHYREXIA", 141 | base_amount: "363.0644948", 142 | base_asset_type: "native", 143 | counter_offer_id: "78448401", 144 | counter_account: "SERRA", 145 | counter_amount: "139.5761839", 146 | counter_asset_type: "credit_alphanum4", 147 | counter_asset_code: "BAT", 148 | counter_asset_issuer: 149 | "GBDEVU63Y6NTHJQQZIKVTC23NWLQVP3WJ2RI2OTSJTNYOIGICST6DUXR", 150 | base_is_seller: false, 151 | price: { 152 | n: 10000000, 153 | d: 26011923, 154 | }, 155 | }, 156 | ], 157 | }, 158 | }; 159 | -------------------------------------------------------------------------------- /src/fixtures/TransferInfoResponse.ts: -------------------------------------------------------------------------------- 1 | export const AnchorUSDTransferInfo = { 2 | deposit: { 3 | USD: { 4 | enabled: true, 5 | fee_fixed: 5, 6 | fee_percent: 1, 7 | min_amount: 15, 8 | fields: { 9 | email_address: { 10 | description: "your email address for transaction status updates", 11 | }, 12 | amount: { description: "amount in USD that you plan to deposit" }, 13 | }, 14 | }, 15 | }, 16 | withdraw: { 17 | USD: { 18 | enabled: true, 19 | fee_fixed: 5, 20 | fee_percent: 2, 21 | min_amount: 15, 22 | types: { 23 | bank_account: { 24 | fields: { 25 | account: { 26 | description: 27 | "the stellar account that you will be transferring funds from", 28 | }, 29 | email_address: { 30 | description: "your email address for transaction status updates", 31 | }, 32 | amount: { 33 | description: "amount in USD that you plan to withdraw", 34 | optional: true, 35 | }, 36 | }, 37 | }, 38 | }, 39 | }, 40 | }, 41 | transactions: { enabled: false }, 42 | }; 43 | 44 | export const ApayTransferInfo = { 45 | deposit: { 46 | BCH: { enabled: true, fee_fixed: 0, min_amount: 0.001 }, 47 | BTC: { enabled: true, fee_fixed: 0, min_amount: 0.0002 }, 48 | DASH: { enabled: false, fee_fixed: 0, min_amount: 0.003 }, 49 | LTC: { enabled: true, fee_fixed: 0, min_amount: 0.01 }, 50 | ETH: { enabled: true, fee_fixed: 0, min_amount: 0.001 }, 51 | BAT: { enabled: true, fee_fixed: 0, min_amount: 2 }, 52 | KIN: { enabled: true, fee_fixed: 0, min_amount: 0 }, 53 | LINK: { enabled: true, fee_fixed: 0, min_amount: 2 }, 54 | MTL: { enabled: true, fee_fixed: 0, min_amount: 0.3 }, 55 | OMG: { enabled: true, fee_fixed: 0, min_amount: 0.1 }, 56 | REP: { enabled: true, fee_fixed: 0, min_amount: 0.02 }, 57 | SALT: { enabled: true, fee_fixed: 0, min_amount: 0.5 }, 58 | ZRX: { enabled: true, fee_fixed: 0, min_amount: 0.5 }, 59 | }, 60 | withdraw: { 61 | BCH: { 62 | enabled: true, 63 | fee_fixed: 0.002, 64 | min_amount: 0.004, 65 | types: { crypto: {} }, 66 | }, 67 | BTC: { 68 | enabled: true, 69 | fee_fixed: 0.0005, 70 | min_amount: 0.001, 71 | types: { crypto: {} }, 72 | }, 73 | DASH: { 74 | enabled: false, 75 | fee_fixed: 0.006, 76 | min_amount: 0.012, 77 | types: { crypto: {} }, 78 | }, 79 | LTC: { 80 | enabled: true, 81 | fee_fixed: 0.025, 82 | min_amount: 0.05, 83 | types: { crypto: {} }, 84 | }, 85 | ETH: { 86 | enabled: true, 87 | fee_fixed: 0.005, 88 | min_amount: 0.01, 89 | types: { crypto: {} }, 90 | }, 91 | BAT: { enabled: true, fee_fixed: 5, min_amount: 10, types: { crypto: {} } }, 92 | KIN: { 93 | enabled: true, 94 | fee_fixed: 10000, 95 | min_amount: 20000, 96 | types: { crypto: {} }, 97 | }, 98 | LINK: { 99 | enabled: true, 100 | fee_fixed: 5, 101 | min_amount: 10, 102 | types: { crypto: {} }, 103 | }, 104 | MTL: { 105 | enabled: true, 106 | fee_fixed: 0.5, 107 | min_amount: 1, 108 | types: { crypto: {} }, 109 | }, 110 | OMG: { 111 | enabled: true, 112 | fee_fixed: 0.2, 113 | min_amount: 0.4, 114 | types: { crypto: {} }, 115 | }, 116 | REP: { 117 | enabled: true, 118 | fee_fixed: 0.05, 119 | min_amount: 0.1, 120 | types: { crypto: {} }, 121 | }, 122 | SALT: { enabled: true, fee_fixed: 1, min_amount: 2, types: { crypto: {} } }, 123 | ZRX: { enabled: true, fee_fixed: 2, min_amount: 4, types: { crypto: {} } }, 124 | }, 125 | transactions: { enabled: false }, 126 | }; 127 | 128 | export const SMXTransferInfo = { 129 | deposit: { 130 | SMX: { 131 | enabled: true, 132 | fee_fixed: 0, 133 | fee_percent: 0, 134 | min_amount: 1500, 135 | max_amount: 1000000, 136 | fields: { 137 | email_address: { 138 | description: "your email address for transaction status updates", 139 | optional: true, 140 | }, 141 | amount: { description: "amount in cents that you plan to deposit" }, 142 | type: { 143 | description: "type of deposit to make", 144 | choices: ["SPEI", "cash"], 145 | }, 146 | }, 147 | }, 148 | }, 149 | withdraw: { 150 | SMX: { 151 | enabled: true, 152 | fee_fixed: 0, 153 | fee_percent: 0, 154 | min_amount: 0.1, 155 | max_amount: 1000000, 156 | types: { 157 | bank_account: { 158 | fields: { dest: { description: "your bank account number" } }, 159 | }, 160 | }, 161 | }, 162 | }, 163 | fee: { enabled: false }, 164 | transactions: { enabled: true }, 165 | transaction: { enabled: true }, 166 | }; 167 | -------------------------------------------------------------------------------- /src/fixtures/keys.ts: -------------------------------------------------------------------------------- 1 | import * as StellarSdk from "@stellar/stellar-sdk"; 2 | 3 | import { EncryptedKey, Key, KeyMetadata } from "../types"; 4 | 5 | import { KeyType } from "../constants/keys"; 6 | 7 | export function generatePlaintextKey(): Key { 8 | const account = StellarSdk.Keypair.random(); 9 | const publicKey = account.publicKey(); 10 | const privateKey = account.secret(); 11 | 12 | return { 13 | id: `${Math.random()}`, 14 | type: KeyType.plaintextKey, 15 | publicKey, 16 | privateKey, 17 | }; 18 | } 19 | 20 | export function generateLedgerKey(): Key { 21 | const account = StellarSdk.Keypair.random(); 22 | const publicKey = account.publicKey(); 23 | 24 | return { 25 | id: `${Math.random()}`, 26 | type: KeyType.ledger, 27 | publicKey, 28 | privateKey: "", 29 | path: "44'/148'/0'", 30 | }; 31 | } 32 | 33 | export function generateEncryptedKey(encrypterName: string): EncryptedKey { 34 | const { privateKey, ...key } = generatePlaintextKey(); 35 | 36 | return { 37 | ...key, 38 | encrypterName, 39 | salt: "", 40 | encryptedBlob: `${privateKey}password`, 41 | }; 42 | } 43 | 44 | export function generateKeyMetadata(encrypterName: string): KeyMetadata { 45 | const { id } = generateEncryptedKey(encrypterName); 46 | 47 | return { 48 | id, 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /src/helpers/ScryptEncryption.test.ts: -------------------------------------------------------------------------------- 1 | import { decrypt, encrypt, NONCE_BYTES } from "./ScryptEncryption"; 2 | 3 | test("encrypts and decrypts a key", async () => { 4 | const privateKey = "ARCHANGEL"; 5 | 6 | const password = "This is a really cool password and is good"; 7 | const salt = "Also this salt is really key, and good"; 8 | const nonce = new Uint8Array(NONCE_BYTES).fill(42); 9 | 10 | const { encryptedPhrase } = await encrypt({ 11 | phrase: privateKey, 12 | password, 13 | salt, 14 | nonce, 15 | }); 16 | 17 | expect(encryptedPhrase).toBeTruthy(); 18 | expect(encryptedPhrase).not.toEqual(privateKey); 19 | expect(encryptedPhrase).toEqual( 20 | "ASoqKioqKioqKioqKioqKioqKioqKioqKghXdTQ4aKmd0WIKwT5YjOCtN95jMeXe1UI=", 21 | ); 22 | 23 | const decryptedPhrase = await decrypt({ 24 | phrase: encryptedPhrase, 25 | password, 26 | salt, 27 | }); 28 | 29 | expect(decryptedPhrase).not.toEqual(encryptedPhrase); 30 | expect(decryptedPhrase).toEqual(privateKey); 31 | }); 32 | 33 | test("encrypts and decrypts a StellarX seed", async () => { 34 | const seed = "SCHKKYK3B3MPQKDTUVS37WSVHJ7EY6YRAGKOMOZJUOOMKVXTHTHBPZVL"; 35 | const password = "hello"; 36 | const salt = "salty"; 37 | const nonce = new Uint8Array(NONCE_BYTES).fill(42); 38 | 39 | const { encryptedPhrase } = await encrypt({ 40 | phrase: seed, 41 | password, 42 | salt, 43 | nonce, 44 | }); 45 | 46 | expect(encryptedPhrase).not.toEqual(seed); 47 | expect(encryptedPhrase).toEqual( 48 | "ASoqKioqKioqKioqKioqKioqKioqKioqKgjCVz5H3mKHykeuO6GA8KKJSQrTu9D9Gt8nhO" + 49 | "R7u3iccJc+jV768SEGOtWnwU6x4o46LxhKI8nQGMahV4JpqruESNW8vwt0OQ==", 50 | ); 51 | 52 | const decryptedPhrase = await decrypt({ 53 | phrase: encryptedPhrase, 54 | password, 55 | salt, 56 | }); 57 | 58 | expect(decryptedPhrase).not.toEqual(encryptedPhrase); 59 | expect(decryptedPhrase).toEqual(seed); 60 | }); 61 | -------------------------------------------------------------------------------- /src/helpers/ScryptEncryption.ts: -------------------------------------------------------------------------------- 1 | import scrypt from "scrypt-async"; 2 | import nacl from "tweetnacl"; 3 | import naclutil from "tweetnacl-util"; 4 | 5 | export interface ScryptPassParams { 6 | password: string; 7 | salt: string; 8 | dkLen?: number; 9 | } 10 | 11 | export interface EncryptParams { 12 | phrase: string; 13 | password: string; 14 | 15 | // these should only be used for testing! 16 | salt?: string; 17 | nonce?: Uint8Array; 18 | } 19 | 20 | export interface EncryptResponse { 21 | encryptedPhrase: string; 22 | salt: string; 23 | } 24 | 25 | export interface DecryptParams { 26 | phrase: string; 27 | password: string; 28 | salt: string; 29 | } 30 | 31 | export const RECOVERY_CODE_NBITS = 160; 32 | export const RECOVERY_CODE_NWORDS = (RECOVERY_CODE_NBITS / 32) * 3; 33 | export const SALT_BYTES = 32; 34 | export const NONCE_BYTES = nacl.secretbox.nonceLength; // 24 bytes 35 | export const LOCAL_KEY_BYTES = nacl.secretbox.keyLength; // 32 bytes 36 | export const CRYPTO_V1 = 1; 37 | export const CURRENT_CRYPTO_VERSION = CRYPTO_V1; 38 | export const KEY_LEN = nacl.secretbox.keyLength; // 32 bytes 39 | 40 | /** 41 | * Convert password from user into a derived key for encryption 42 | * @param {string} param.password plaintext password from user 43 | * @param {string} param.salt salt (should be randomly generated) 44 | * @param {number} param.dkLen length of the derived key to output 45 | * @returns {Uint8Array} bytes of the derived key 46 | */ 47 | function scryptPass(params: ScryptPassParams): Promise { 48 | const { password, salt, dkLen = KEY_LEN } = params; 49 | const [N, r, p] = [32768, 8, 1]; 50 | return new Promise((resolve, reject) => { 51 | scrypt( 52 | password, 53 | salt, 54 | { N, r, p, dkLen, encoding: "binary" }, 55 | (derivedKey: Uint8Array) => { 56 | if (derivedKey) { 57 | resolve(derivedKey); 58 | } else { 59 | reject(new Error("scryptPass failed, derivedKey is null")); 60 | } 61 | }, 62 | ); 63 | }); 64 | } 65 | 66 | function generateSalt(): string { 67 | return naclutil.encodeBase64(nacl.randomBytes(SALT_BYTES)); 68 | } 69 | 70 | /** 71 | * Encrypt a phrase using scrypt. 72 | * @async 73 | * @param {Object} params Params object 74 | * @param {string} params.phrase Phrase to be encrypted 75 | * @param {string} params.password A password to encrypt the string with. 76 | * @param {string} [params.salt] A static salt. Use only for unit tests. 77 | * @param {string} [params.nonce] A static nonce. Use only for unit tests. 78 | */ 79 | export async function encrypt(params: EncryptParams): Promise { 80 | const { phrase, password, salt, nonce } = params; 81 | const secretboxSalt = salt || generateSalt(); 82 | 83 | const secretboxNonce = nonce || nacl.randomBytes(NONCE_BYTES); 84 | const scryptedPass = await scryptPass({ password, salt: secretboxSalt }); 85 | const textBytes = naclutil.decodeUTF8(phrase); 86 | const cipherText = nacl.secretbox(textBytes, secretboxNonce, scryptedPass); 87 | 88 | if (!cipherText) { 89 | throw new Error("Encryption failed"); 90 | } 91 | 92 | // merge these into one array 93 | // (in a somewhat ugly way, since TS doesn't like destructuring Uint8Arrays) 94 | const bundle = new Uint8Array(1 + secretboxNonce.length + cipherText.length); 95 | bundle.set([CURRENT_CRYPTO_VERSION]); 96 | bundle.set(secretboxNonce, 1); 97 | bundle.set(cipherText, 1 + secretboxNonce.length); 98 | 99 | return { 100 | encryptedPhrase: naclutil.encodeBase64(bundle), 101 | salt: secretboxSalt, 102 | }; 103 | } 104 | 105 | export async function decrypt(params: DecryptParams): Promise { 106 | const { phrase, password, salt } = params; 107 | const scryptedPass = await scryptPass({ password, salt }); 108 | 109 | const bundle = naclutil.decodeBase64(phrase); 110 | const version = bundle[0]; 111 | let decryptedBytes; 112 | if (version === CRYPTO_V1) { 113 | const nonce = bundle.slice(1, 1 + NONCE_BYTES); 114 | const cipherText = bundle.slice(1 + NONCE_BYTES); 115 | decryptedBytes = nacl.secretbox.open(cipherText, nonce, scryptedPass); 116 | } else { 117 | throw new Error(`Cipher version ${version} not supported.`); 118 | } 119 | if (!decryptedBytes) { 120 | throw new Error("That passphrase wasn’t valid."); 121 | } 122 | return naclutil.encodeUTF8(decryptedBytes); 123 | } 124 | -------------------------------------------------------------------------------- /src/helpers/bigize.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from "bignumber.js"; 2 | 3 | import isArray from "lodash/isArray"; 4 | import isObject from "lodash/isObject"; 5 | 6 | import { KeyMap } from "../types"; 7 | 8 | /** 9 | * Given a list of key names to bigize, traverse an object (usually, an 10 | * API response) for those keys, and convert their values to a BigNumber 11 | * (if it's a valid value). 12 | */ 13 | export function bigize(obj: any, keys: string[] = []): any { 14 | const keyMap: KeyMap = keys.reduce( 15 | (memo, key) => ({ ...memo, [key]: true }), 16 | {}, 17 | ); 18 | 19 | if (isArray(obj)) { 20 | return obj.map((o) => bigize(o, keys)); 21 | } 22 | 23 | if (!isObject(obj)) { 24 | return obj; 25 | } 26 | 27 | if (obj instanceof BigNumber) { 28 | return obj; 29 | } 30 | 31 | return Object.keys(obj).reduce((memo: object, key: string): object => { 32 | if (keyMap[key] && typeof (obj as KeyMap)[key] !== "object") { 33 | return { 34 | ...memo, 35 | [key]: 36 | (obj as KeyMap)[key] === null || (obj as KeyMap)[key] === undefined 37 | ? (obj as KeyMap)[key] 38 | : new BigNumber((obj as KeyMap)[key]).decimalPlaces( 39 | 7, 40 | BigNumber.ROUND_HALF_UP, 41 | ), 42 | }; 43 | } 44 | 45 | return { 46 | ...memo, 47 | [key]: bigize((obj as KeyMap)[key], keys), 48 | }; 49 | }, {}); 50 | } 51 | -------------------------------------------------------------------------------- /src/helpers/getKeyMetadata.test.ts: -------------------------------------------------------------------------------- 1 | import { EncryptedKey } from "../types"; 2 | 3 | import { getKeyMetadata } from "./getKeyMetadata"; 4 | 5 | describe("getKeyMetadata", () => { 6 | test("ledger key", () => { 7 | const encryptedKey: EncryptedKey = { 8 | id: "PURIFIER", 9 | encryptedBlob: "BLOB", 10 | encrypterName: "Test", 11 | salt: "SLFKJSDLKFJLSKDJFLKSJD", 12 | }; 13 | 14 | expect(getKeyMetadata(encryptedKey)).toEqual({ 15 | id: "PURIFIER", 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/helpers/getKeyMetadata.ts: -------------------------------------------------------------------------------- 1 | import { EncryptedKey, KeyMetadata } from "../types"; 2 | 3 | export function getKeyMetadata(encryptedKey: EncryptedKey): KeyMetadata { 4 | const { id } = encryptedKey; 5 | 6 | return { 7 | id, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Types 3 | */ 4 | import * as Types from "./types"; 5 | 6 | export { Types }; 7 | 8 | /** 9 | * Constants 10 | */ 11 | export { EffectType } from "./constants/data"; 12 | export { KeyType } from "./constants/keys"; 13 | export { TransferResponseType, TransactionStatus } from "./constants/transfers"; 14 | export { ApprovalResponseStatus, ActionResult } from "./constants/sep8"; 15 | 16 | /** 17 | * Data 18 | */ 19 | export { 20 | getBalanceIdentifier, 21 | getTokenIdentifier, 22 | getStellarSdkAsset, 23 | } from "./data"; 24 | 25 | export { DataProvider } from "./data/DataProvider"; 26 | 27 | /** 28 | * Key Management 29 | */ 30 | export { KeyManager } from "./KeyManager"; 31 | 32 | export { KeyManagerPlugins } from "./KeyManagerPlugins"; 33 | 34 | /** 35 | * Plugin Testing 36 | */ 37 | 38 | export { testEncrypter, testKeyStore } from "./PluginTesting"; 39 | 40 | /** 41 | * Transfers 42 | */ 43 | export { DepositProvider, getKycUrl, WithdrawProvider } from "./transfers"; 44 | 45 | /** 46 | * Helpers 47 | */ 48 | export { getKeyMetadata } from "./helpers/getKeyMetadata"; 49 | -------------------------------------------------------------------------------- /src/keyTypeHandlers/albedo.ts: -------------------------------------------------------------------------------- 1 | import albedo from "@albedo-link/intent"; 2 | import { 3 | Networks, 4 | Transaction, 5 | TransactionBuilder, 6 | } from "@stellar/stellar-sdk"; 7 | 8 | import { HandlerSignTransactionParams, KeyTypeHandler } from "../types"; 9 | 10 | import { KeyType } from "../constants/keys"; 11 | 12 | export const albedoHandler: KeyTypeHandler = { 13 | keyType: KeyType.albedo, 14 | async signTransaction(params: HandlerSignTransactionParams) { 15 | const { transaction, key } = params; 16 | 17 | if (key.privateKey !== "") { 18 | throw new Error( 19 | `Non-ledger key sent to ledger handler: ${JSON.stringify( 20 | key.publicKey, 21 | )}`, 22 | ); 23 | } 24 | 25 | try { 26 | const xdr = transaction.toXDR(); 27 | const response = await albedo.tx({ xdr }); 28 | 29 | if (!response.signed_envelope_xdr) { 30 | throw new Error("We couldn’t sign the transaction with Albedo."); 31 | } 32 | 33 | // fromXDR() returns type "Transaction | FeeBumpTransaction" and 34 | // signTransaction() doesn't like "| FeeBumpTransaction" type, so casting 35 | // to "Transaction" type. 36 | return TransactionBuilder.fromXDR( 37 | response.signed_envelope_xdr, 38 | Networks.PUBLIC, 39 | ) as Transaction; 40 | } catch (error) { 41 | const errorMsg = (error as any).toString(); 42 | throw new Error( 43 | `We couldn’t sign the transaction with Albedo. ${errorMsg}.`, 44 | ); 45 | } 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /src/keyTypeHandlers/freighter.test.ts: -------------------------------------------------------------------------------- 1 | import freighterApi from "@stellar/freighter-api"; 2 | import { TransactionBuilder } from "@stellar/stellar-sdk"; 3 | import sinon from "sinon"; 4 | import { freighterHandler } from "./freighter"; 5 | 6 | describe("freighterHandler", () => { 7 | const XDR = "foo"; 8 | const NETWORK = "baz"; 9 | const SIGNED_TRANSACTION = "xxx"; 10 | 11 | let freighterApiMock: sinon.SinonMock; 12 | let TransactionBuilderMock: sinon.SinonMock; 13 | beforeEach(() => { 14 | freighterApiMock = sinon.mock(freighterApi); 15 | TransactionBuilderMock = sinon.mock(TransactionBuilder); 16 | }); 17 | afterEach(() => { 18 | freighterApiMock.verify(); 19 | freighterApiMock.restore(); 20 | TransactionBuilderMock.verify(); 21 | TransactionBuilderMock.restore(); 22 | }); 23 | test("signTransaction is called with network", () => { 24 | freighterApiMock 25 | .expects("signTransaction") 26 | .once() 27 | .withArgs(XDR, NETWORK) 28 | .returns(Promise.resolve(SIGNED_TRANSACTION)); 29 | TransactionBuilderMock.expects("fromXDR") 30 | .once() 31 | .withArgs(SIGNED_TRANSACTION) 32 | .returns(true); 33 | 34 | freighterHandler.signTransaction({ 35 | // @ts-ignore 36 | transaction: { toXDR: () => XDR }, 37 | // @ts-ignore 38 | key: { privateKey: "" }, 39 | custom: { network: NETWORK }, 40 | }); 41 | }); 42 | test("signTransaction is called without network", () => { 43 | freighterApiMock 44 | .expects("signTransaction") 45 | .once() 46 | .withArgs(XDR, undefined) 47 | .returns(Promise.resolve(SIGNED_TRANSACTION)); 48 | TransactionBuilderMock.expects("fromXDR") 49 | .once() 50 | .returns(true); 51 | 52 | freighterHandler.signTransaction({ 53 | // @ts-ignore 54 | transaction: { toXDR: () => XDR }, 55 | // @ts-ignore 56 | key: { privateKey: "" }, 57 | }); 58 | }); 59 | test("falsy config is passed as undefined to signTransaction", () => { 60 | freighterApiMock 61 | .expects("signTransaction") 62 | .once() 63 | .withArgs(XDR, undefined) 64 | .returns(Promise.resolve(SIGNED_TRANSACTION)); 65 | TransactionBuilderMock.expects("fromXDR") 66 | .once() 67 | .withArgs(SIGNED_TRANSACTION) 68 | .returns(true); 69 | 70 | freighterHandler.signTransaction({ 71 | // @ts-ignore 72 | transaction: { toXDR: () => XDR }, 73 | // @ts-ignore 74 | key: { privateKey: "" }, 75 | // @ts-ignore 76 | custom: false, 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/keyTypeHandlers/freighter.ts: -------------------------------------------------------------------------------- 1 | import freighterApi from "@stellar/freighter-api"; 2 | import { 3 | Networks, 4 | Transaction, 5 | TransactionBuilder, 6 | } from "@stellar/stellar-sdk"; 7 | 8 | import { HandlerSignTransactionParams, KeyTypeHandler } from "../types"; 9 | 10 | import { KeyType } from "../constants/keys"; 11 | 12 | export const freighterHandler: KeyTypeHandler = { 13 | keyType: KeyType.freighter, 14 | async signTransaction(params: HandlerSignTransactionParams) { 15 | const { transaction, key, custom } = params; 16 | 17 | if (key.privateKey !== "") { 18 | throw new Error( 19 | `Non-ledger key sent to ledger handler: ${JSON.stringify( 20 | key.publicKey, 21 | )}`, 22 | ); 23 | } 24 | 25 | try { 26 | const response = await freighterApi.signTransaction( 27 | transaction.toXDR(), 28 | custom && custom.network ? custom.network : undefined, 29 | ); 30 | 31 | // fromXDR() returns type "Transaction | FeeBumpTransaction" and 32 | // signTransaction() doesn't like "| FeeBumpTransaction" type, so casting 33 | // to "Transaction" type. 34 | return TransactionBuilder.fromXDR( 35 | response, 36 | Networks.PUBLIC, 37 | ) as Transaction; 38 | } catch (error) { 39 | const errorMsg = (error as any).toString(); 40 | throw new Error( 41 | `We couldn’t sign the transaction with Freighter. ${errorMsg}.`, 42 | ); 43 | } 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /src/keyTypeHandlers/ledger.ts: -------------------------------------------------------------------------------- 1 | import LedgerStr from "@ledgerhq/hw-app-str"; 2 | import LedgerTransport from "@ledgerhq/hw-transport-u2f"; 3 | import { Keypair, xdr } from "@stellar/stellar-sdk"; 4 | 5 | import { HandlerSignTransactionParams, KeyTypeHandler } from "../types"; 6 | 7 | import { KeyType } from "../constants/keys"; 8 | 9 | export const ledgerHandler: KeyTypeHandler = { 10 | keyType: KeyType.ledger, 11 | async signTransaction(params: HandlerSignTransactionParams) { 12 | const { transaction, key } = params; 13 | 14 | if (key.privateKey !== "") { 15 | throw new Error( 16 | `Non-ledger key sent to ledger handler: ${JSON.stringify( 17 | key.publicKey, 18 | )}`, 19 | ); 20 | } 21 | 22 | /* 23 | There's a naive way to do this (to keep all functions stateless and 24 | make the connection anew each time), and there's some way of weaving state 25 | into this. 26 | 27 | Gonna do the naive thing first and then figure out how to do this right. 28 | */ 29 | const transport = await LedgerTransport.create(60 * 1000); 30 | const ledgerApi = new LedgerStr(transport); 31 | const result = await ledgerApi.signTransaction( 32 | key.path, 33 | transaction.signatureBase(), 34 | ); 35 | 36 | const keyPair = Keypair.fromPublicKey(key.publicKey); 37 | const decoratedSignature = new xdr.DecoratedSignature({ 38 | hint: keyPair.signatureHint(), 39 | signature: result.signature, 40 | }); 41 | transaction.signatures.push(decoratedSignature); 42 | 43 | return Promise.resolve(transaction); 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /src/keyTypeHandlers/plaintextKey.ts: -------------------------------------------------------------------------------- 1 | import { Keypair } from "@stellar/stellar-sdk"; 2 | 3 | import { HandlerSignTransactionParams, KeyTypeHandler } from "../types"; 4 | 5 | import { KeyType } from "../constants/keys"; 6 | 7 | export const plaintextKeyHandler: KeyTypeHandler = { 8 | keyType: KeyType.plaintextKey, 9 | signTransaction(params: HandlerSignTransactionParams) { 10 | const { transaction, key } = params; 11 | if (key.privateKey === "") { 12 | throw new Error( 13 | `Non-plaintext key sent to plaintext handler: ${JSON.stringify( 14 | key.publicKey, 15 | )}`, 16 | ); 17 | } 18 | 19 | const keyPair = Keypair.fromSecret(key.privateKey); 20 | 21 | /* 22 | * NOTE: we need to use the combo of getKeypairSignature() + addSignature() 23 | * here in place of the shorter sign() call because sign() results in a 24 | * "XDR Write Error: [object Object] is not a DecoratedSignature" error 25 | * on React Native whenever we try to call transaction.toXDR() on the signed 26 | * transaction. 27 | */ 28 | 29 | const signature = transaction.getKeypairSignature(keyPair); 30 | transaction.addSignature(keyPair.publicKey(), signature); 31 | 32 | return Promise.resolve(transaction); 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /src/keyTypeHandlers/trezor.ts: -------------------------------------------------------------------------------- 1 | import TrezorConnect from "trezor-connect"; 2 | 3 | import { HandlerSignTransactionParams, KeyTypeHandler } from "../types"; 4 | 5 | import { KeyType } from "../constants/keys"; 6 | import { transformTransaction } from "../helpers/trezorTransformTransaction"; 7 | 8 | export const trezorHandler: KeyTypeHandler = { 9 | keyType: KeyType.trezor, 10 | async signTransaction(params: HandlerSignTransactionParams) { 11 | const { transaction, key, custom } = params; 12 | 13 | if (key.privateKey !== "") { 14 | throw new Error( 15 | `Non-ledger key sent to ledger handler: ${JSON.stringify( 16 | key.publicKey, 17 | )}`, 18 | ); 19 | } 20 | 21 | if (!custom || !custom.email || !custom.appUrl) { 22 | throw new Error( 23 | `Trezor Connect manifest with "email" and "appUrl" props is required. 24 | Make sure they are passed through "custom" prop.`, 25 | ); 26 | } 27 | 28 | try { 29 | TrezorConnect.manifest({ 30 | email: custom.email, 31 | appUrl: custom.appUrl, 32 | }); 33 | 34 | const trezorParams = transformTransaction("m/44'/148'/0'", transaction); 35 | const response = await TrezorConnect.stellarSignTransaction(trezorParams); 36 | 37 | if (response.success) { 38 | const signature = Buffer.from( 39 | response.payload.signature, 40 | "hex", 41 | ).toString("base64"); 42 | transaction.addSignature(key.publicKey, signature); 43 | 44 | return transaction; 45 | } 46 | 47 | throw new Error( 48 | response.payload.error || 49 | "We couldn’t sign the transaction with Trezor.", 50 | ); 51 | } catch (error) { 52 | const errorMsg = (error as any).toString(); 53 | throw new Error( 54 | `We couldn’t sign the transaction with Trezor. ${errorMsg}.`, 55 | ); 56 | } 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /src/plugins/BrowserStorageFacade.ts: -------------------------------------------------------------------------------- 1 | import { EncryptedKey } from "../types"; 2 | 3 | export interface BrowserStorageConfigParams { 4 | prefix?: string; 5 | storage: { 6 | get: (key?: string | string[] | {}) => Promise<{}>; 7 | remove: (key: string | string[]) => Promise; 8 | set: (items: {}) => Promise<{}>; 9 | }; 10 | } 11 | 12 | const PREFIX = "stellarkeys"; 13 | 14 | /** 15 | * Facade for `BrowserStorageKeyStore` encapsulating the access to the actual 16 | * browser storage 17 | */ 18 | export class BrowserStorageFacade { 19 | private storage: Storage | null; 20 | private prefix: string; 21 | 22 | constructor() { 23 | this.storage = null; 24 | this.prefix = PREFIX; 25 | } 26 | 27 | public configure(params: BrowserStorageConfigParams) { 28 | Object.assign(this, params); 29 | } 30 | 31 | public async hasKey(id: string) { 32 | this.check(); 33 | 34 | return this.storage !== null 35 | ? !!Object.keys(await this.storage.get(`${this.prefix}:${id}`)).length 36 | : null; 37 | } 38 | 39 | public async getKey(id: string) { 40 | this.check(); 41 | const key = `${this.prefix}:${id}`; 42 | const itemObj = this.storage !== null ? await this.storage.get(key) : null; 43 | 44 | const item = itemObj[key]; 45 | return item || null; 46 | } 47 | 48 | public setKey(id: string, key: EncryptedKey) { 49 | this.check(); 50 | return this.storage !== null 51 | ? this.storage.set({ [`${this.prefix}:${id}`]: { ...key } }) 52 | : null; 53 | } 54 | 55 | public removeKey(id: string) { 56 | this.check(); 57 | return this.storage !== null 58 | ? this.storage.remove(`${this.prefix}:${id}`) 59 | : null; 60 | } 61 | 62 | public async getAllKeys() { 63 | this.check(); 64 | const regexp = RegExp(`^${PREFIX}\\:(.*)`); 65 | const keys: EncryptedKey[] = []; 66 | 67 | if (this.storage !== null) { 68 | const storageObj = await this.storage.get(null); 69 | const storageKeys = Object.keys(storageObj); 70 | for (const storageKey of storageKeys) { 71 | const raw_id = storageKey; 72 | if (raw_id !== null && regexp.test(raw_id)) { 73 | const key = await this.getKey(regexp.exec(raw_id)![1]); 74 | if (key !== null) { 75 | keys.push(key); 76 | } 77 | } 78 | } 79 | } 80 | return keys; 81 | } 82 | 83 | private check() { 84 | if (this.storage === null) { 85 | throw new Error("A storage object must have been set"); 86 | } 87 | if (this.prefix === "") { 88 | throw new Error("A non-empty prefix must have been set"); 89 | } 90 | return this.storage !== null && this.prefix !== ""; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/plugins/BrowserStorageKeyStore.test.ts: -------------------------------------------------------------------------------- 1 | import sinon from "sinon"; 2 | 3 | import { EncryptedKey } from "../types"; 4 | import { BrowserStorageKeyStore } from "./BrowserStorageKeyStore"; 5 | 6 | // tslint:disable-next-line 7 | describe("BrowserStorageKeyStore", function() { 8 | let clock: sinon.SinonFakeTimers; 9 | let testStore: BrowserStorageKeyStore; 10 | const encryptedKey: EncryptedKey = { 11 | id: "PURIFIER", 12 | encryptedBlob: "BLOB", 13 | encrypterName: "Test", 14 | salt: "SLFKJSDLKFJLSKDJFLKSJD", 15 | }; 16 | const keyMetadata = { 17 | id: "PURIFIER", 18 | }; 19 | const chrome = { 20 | storage: { 21 | local: { 22 | get: (_key?: string | string[] | {}) => Promise.resolve({}), 23 | set: (_items: {}) => Promise.resolve({}), 24 | remove: (_key: string | string[]) => Promise.resolve(), 25 | }, 26 | }, 27 | }; 28 | 29 | beforeEach(() => { 30 | clock = sinon.useFakeTimers(666); 31 | testStore = new BrowserStorageKeyStore(); 32 | 33 | testStore.configure({ storage: chrome.storage.local }); 34 | }); 35 | 36 | afterEach(() => { 37 | clock.restore(); 38 | sinon.restore(); 39 | }); 40 | 41 | it("properly stores keys", async () => { 42 | const chromeStorageLocalGetStub = sinon.stub(chrome.storage.local, "get"); 43 | 44 | /* first call returns empty to confirm keystore 45 | doesn't already exist before storing */ 46 | chromeStorageLocalGetStub.onCall(0).returns(Promise.resolve({})); 47 | const testMetadata = await testStore.storeKeys([encryptedKey]); 48 | 49 | expect(testMetadata).toEqual([keyMetadata]); 50 | 51 | // subsequent calls return the keystore as expected 52 | chromeStorageLocalGetStub.returns( 53 | Promise.resolve({ [`stellarkeys:${encryptedKey.id}`]: encryptedKey }), 54 | ); 55 | const allKeys = await testStore.loadAllKeys(); 56 | 57 | expect(allKeys).toEqual([{ ...encryptedKey, ...keyMetadata }]); 58 | }); 59 | 60 | it("properly deletes keys", async () => { 61 | const chromeStorageLocalGetStub = sinon.stub(chrome.storage.local, "get"); 62 | 63 | /* first call returns empty to confirm keystore 64 | doesn't already exist before storing */ 65 | chromeStorageLocalGetStub.onCall(0).returns(Promise.resolve({})); 66 | await testStore.storeKeys([encryptedKey]); 67 | 68 | // subsequent calls return the keystore as expected 69 | chromeStorageLocalGetStub.returns( 70 | Promise.resolve({ [`stellarkeys:${encryptedKey.id}`]: encryptedKey }), 71 | ); 72 | 73 | const allKeys = await testStore.loadAllKeys(); 74 | 75 | expect(allKeys).toEqual([{ ...encryptedKey, ...keyMetadata }]); 76 | 77 | const removalMetadata = await testStore.removeKey("PURIFIER"); 78 | chromeStorageLocalGetStub.returns(Promise.resolve({})); 79 | 80 | expect(removalMetadata).toEqual(keyMetadata); 81 | const noKeys = await testStore.loadAllKeys(); 82 | 83 | expect(noKeys).toEqual([]); 84 | }); 85 | 86 | it("passes PluginTesting", () => { 87 | /* 88 | TODO: 89 | this test cannot currently be run because we 90 | don't have an adequate way to stub chrome.local.storage yet */ 91 | // testKeyStore(testStore) 92 | // .then(() => { 93 | // done(); 94 | // }) 95 | // .catch(done); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /src/plugins/BrowserStorageKeyStore.ts: -------------------------------------------------------------------------------- 1 | import { getKeyMetadata } from "../helpers/getKeyMetadata"; 2 | import { EncryptedKey, KeyMetadata, KeyStore } from "../types"; 3 | import { 4 | BrowserStorageConfigParams, 5 | BrowserStorageFacade, 6 | } from "./BrowserStorageFacade"; 7 | 8 | /** 9 | * KeyStore for Chrome and Firefox browser storage API: 10 | * https://developer.chrome.com/docs/extensions/reference/storage/. 11 | * Once instantiated and configured, pass it to the `KeyManager` contructor to 12 | * handle the storage of encrypted keys. 13 | * ```js 14 | * const browserKeyStore = new KeyManagerPlugins.BrowserStorageKeyStore(); 15 | * browserKeyStore.configure({ storage: chrome.storage.local }); 16 | * const keyManager = new KeyManager({ 17 | * keyStore: browserKeyStore 18 | * }); 19 | * ``` 20 | */ 21 | export class BrowserStorageKeyStore implements KeyStore { 22 | public name: string; 23 | private keyStore: BrowserStorageFacade; 24 | 25 | constructor() { 26 | this.name = "BrowserStorageKeyStore"; 27 | this.keyStore = new BrowserStorageFacade(); 28 | } 29 | 30 | /** 31 | * The configuration is where the storage engine is set up and configured. 32 | * It must follow the Storage interface : 33 | * https://developer.chrome.com/docs/extensions/reference/storage/). 34 | * This is mostly for use with Chrome and Firefox storage in addition to 35 | * libraries that shim this (for ex: webextension-polyfill) 36 | * @param {BrowserStorageConfigParams} params A configuration object. 37 | * @param {Storage} params.storage The Storage instance. Required. 38 | * @param {string} [params.prefix] The prefix for the names in the storage. 39 | * @return {Promise} 40 | */ 41 | public configure(params: BrowserStorageConfigParams) { 42 | try { 43 | this.keyStore.configure(params); 44 | return Promise.resolve(); 45 | } catch (e) { 46 | return Promise.reject(e); 47 | } 48 | } 49 | 50 | public async storeKeys(keys: EncryptedKey[]) { 51 | // We can't store keys if they're already there 52 | const usedKeys: EncryptedKey[] = []; 53 | 54 | for (const encryptedKey of keys) { 55 | const hasKey = await this.keyStore.hasKey(encryptedKey.id); 56 | if (hasKey) { 57 | usedKeys.push(encryptedKey); 58 | } 59 | } 60 | 61 | if (usedKeys.length) { 62 | return Promise.reject( 63 | `Some keys were already stored in the keystore: ${usedKeys 64 | .map((k) => k.id) 65 | .join(", ")}`, 66 | ); 67 | } 68 | 69 | const keysMetadata: KeyMetadata[] = []; 70 | 71 | for (const encryptedKey of keys) { 72 | this.keyStore.setKey(encryptedKey.id, encryptedKey); 73 | keysMetadata.push(getKeyMetadata(encryptedKey)); 74 | } 75 | 76 | return Promise.resolve(keysMetadata); 77 | } 78 | 79 | public updateKeys(keys: EncryptedKey[]) { 80 | // we can't update keys if they're already stored 81 | const invalidKeys: EncryptedKey[] = keys.filter( 82 | async (encryptedKey: EncryptedKey) => 83 | !(await this.keyStore.hasKey(encryptedKey.id)), 84 | ); 85 | 86 | if (invalidKeys.length) { 87 | return Promise.reject( 88 | `Some keys couldn't be found in the keystore: ${invalidKeys 89 | .map((k) => k.id) 90 | .join(", ")}`, 91 | ); 92 | } 93 | 94 | const keysMetadata = keys.map((encryptedKey: EncryptedKey) => { 95 | this.keyStore.setKey(encryptedKey.id, encryptedKey); 96 | return getKeyMetadata(encryptedKey); 97 | }); 98 | 99 | return Promise.resolve(keysMetadata); 100 | } 101 | 102 | public async loadKey(id: string) { 103 | const key = await this.keyStore.getKey(id); 104 | if (!key) { 105 | return Promise.reject(id); 106 | } 107 | return Promise.resolve(key); 108 | } 109 | 110 | public async removeKey(id: string) { 111 | if (!this.keyStore.hasKey(id)) { 112 | return Promise.reject(id); 113 | } 114 | 115 | const key = await this.keyStore.getKey(id); 116 | const metadata: KeyMetadata = getKeyMetadata(key); 117 | this.keyStore.removeKey(id); 118 | 119 | return Promise.resolve(metadata); 120 | } 121 | 122 | public async loadAllKeys() { 123 | const keys = await this.keyStore.getAllKeys(); 124 | return Promise.resolve(keys); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/plugins/IdentityEncrypter.test.ts: -------------------------------------------------------------------------------- 1 | import { KeyType } from "../constants/keys"; 2 | import { EncryptedKey, Key } from "../types"; 3 | 4 | import { IdentityEncrypter } from "./IdentityEncrypter"; 5 | 6 | import { testEncrypter } from "../PluginTesting"; 7 | 8 | const key: Key = { 9 | id: "PURIFIER", 10 | type: KeyType.plaintextKey, 11 | publicKey: "AVACYN", 12 | privateKey: "ARCHANGEL", 13 | }; 14 | 15 | const encryptedKey: EncryptedKey = { 16 | id: "PURIFIER", 17 | encryptedBlob: JSON.stringify({ 18 | type: KeyType.plaintextKey, 19 | publicKey: "AVACYN", 20 | privateKey: "ARCHANGEL", 21 | }), 22 | encrypterName: "IdentityEncrypter", 23 | salt: "identity", 24 | }; 25 | 26 | it("encrypts to itself", async () => { 27 | expect(await IdentityEncrypter.encryptKey({ key, password: "" })).toEqual( 28 | encryptedKey, 29 | ); 30 | }); 31 | 32 | it("decrypts to itself", async () => { 33 | expect( 34 | await IdentityEncrypter.decryptKey({ encryptedKey, password: "" }), 35 | ).toEqual(key); 36 | }); 37 | 38 | it("passes PluginTesting", async () => { 39 | expect(await testEncrypter(IdentityEncrypter)).toEqual(true); 40 | }); 41 | -------------------------------------------------------------------------------- /src/plugins/IdentityEncrypter.ts: -------------------------------------------------------------------------------- 1 | import { DecryptParams, Encrypter, EncryptParams } from "../types"; 2 | 3 | const NAME = "IdentityEncrypter"; 4 | 5 | /** 6 | * "Encrypt" keys in a very basic, naive way. 7 | */ 8 | export const IdentityEncrypter: Encrypter = { 9 | name: NAME, 10 | encryptKey(params: EncryptParams) { 11 | const { key } = params; 12 | const { type, privateKey, publicKey, path, extra, ...props } = key; 13 | 14 | return Promise.resolve({ 15 | ...props, 16 | encryptedBlob: JSON.stringify({ 17 | type, 18 | publicKey, 19 | privateKey, 20 | path, 21 | extra, 22 | }), 23 | encrypterName: NAME, 24 | salt: "identity", 25 | }); 26 | }, 27 | 28 | decryptKey(params: DecryptParams) { 29 | const { encryptedKey } = params; 30 | const { 31 | encrypterName, 32 | salt, 33 | encryptedBlob, 34 | ...props 35 | } = encryptedKey as any; 36 | 37 | const data = JSON.parse(encryptedBlob); 38 | 39 | return Promise.resolve({ ...props, ...data }); 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /src/plugins/LocalStorageFacade.ts: -------------------------------------------------------------------------------- 1 | import { EncryptedKey } from "../types"; 2 | 3 | export interface LocalStorageConfigParams { 4 | prefix?: string; 5 | storage: Storage; 6 | } 7 | 8 | /** 9 | * Facade for `LocalStorageKeyStore` encapsulating the access to the actual 10 | * local storage 11 | */ 12 | export class LocalStorageFacade { 13 | private storage: Storage | null; 14 | private prefix: string; 15 | 16 | constructor() { 17 | this.storage = null; 18 | this.prefix = "stellarkeys"; 19 | } 20 | 21 | public configure(params: LocalStorageConfigParams) { 22 | Object.assign(this, params); 23 | } 24 | 25 | public hasKey(id: string) { 26 | this.check(); 27 | return this.storage !== null 28 | ? this.storage.getItem(`${this.prefix}:${id}`) !== null 29 | : null; 30 | } 31 | 32 | public getKey(id: string) { 33 | this.check(); 34 | const item = 35 | this.storage !== null 36 | ? this.storage.getItem(`${this.prefix}:${id}`) 37 | : null; 38 | return item ? JSON.parse(item) : null; 39 | } 40 | 41 | public setKey(id: string, key: EncryptedKey) { 42 | this.check(); 43 | return this.storage !== null 44 | ? this.storage.setItem(`${this.prefix}:${id}`, JSON.stringify({ ...key })) 45 | : null; 46 | } 47 | 48 | public removeKey(id: string) { 49 | this.check(); 50 | return this.storage !== null 51 | ? this.storage.removeItem(`${this.prefix}:${id}`) 52 | : null; 53 | } 54 | 55 | public getAllKeys() { 56 | this.check(); 57 | const regexp = RegExp(`^${this.prefix}\\:(.*)`); 58 | const keys: EncryptedKey[] = []; 59 | 60 | if (this.storage !== null) { 61 | for (let i = 0; i < this.storage.length; i++) { 62 | const raw_id = this.storage.key(i); 63 | if (raw_id !== null && regexp.test(raw_id)) { 64 | const key = this.getKey(regexp.exec(raw_id)![1]); 65 | if (key !== null) { 66 | keys.push(key); 67 | } 68 | } 69 | } 70 | } 71 | return keys; 72 | } 73 | 74 | private check() { 75 | if (this.storage === null) { 76 | throw new Error("A storage object must have been set"); 77 | } 78 | if (this.prefix === "") { 79 | throw new Error("A non-empty prefix must have been set"); 80 | } 81 | return this.storage !== null && this.prefix !== ""; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/plugins/LocalStorageKeyStore.test.ts: -------------------------------------------------------------------------------- 1 | import sinon from "sinon"; 2 | 3 | import { testKeyStore } from "../PluginTesting"; 4 | import { EncryptedKey } from "../types"; 5 | import { LocalStorageKeyStore } from "./LocalStorageKeyStore"; 6 | 7 | import { LocalStorage } from "node-localstorage"; 8 | import os from "os"; 9 | import path from "path"; 10 | 11 | // tslint:disable-next-line 12 | describe("LocalStorageKeyStore", function() { 13 | let clock: sinon.SinonFakeTimers; 14 | let testStore: LocalStorageKeyStore; 15 | let localStorage: Storage; 16 | 17 | beforeEach(() => { 18 | clock = sinon.useFakeTimers(666); 19 | testStore = new LocalStorageKeyStore(); 20 | localStorage = new LocalStorage( 21 | path.resolve(os.tmpdir(), "js-stellar-wallets"), 22 | ); 23 | testStore.configure({ storage: localStorage }); 24 | }); 25 | 26 | afterEach(() => { 27 | clock.restore(); 28 | localStorage.clear(); 29 | }); 30 | 31 | it("properly stores keys", async () => { 32 | const encryptedKey: EncryptedKey = { 33 | id: "PURIFIER", 34 | encryptedBlob: "BLOB", 35 | encrypterName: "Test", 36 | salt: "SLFKJSDLKFJLSKDJFLKSJD", 37 | }; 38 | 39 | const keyMetadata = { 40 | id: "PURIFIER", 41 | }; 42 | 43 | const testMetadata = await testStore.storeKeys([encryptedKey]); 44 | 45 | expect(testMetadata).toEqual([keyMetadata]); 46 | 47 | const allKeys = await testStore.loadAllKeys(); 48 | 49 | expect(allKeys).toEqual([{ ...encryptedKey, ...keyMetadata }]); 50 | }); 51 | 52 | it("properly deletes keys", async () => { 53 | const encryptedKey: EncryptedKey = { 54 | id: "PURIFIER", 55 | encrypterName: "Test", 56 | encryptedBlob: "BLOB", 57 | salt: "SLFKJSDLKFJLSKDJFLKSJD", 58 | }; 59 | 60 | const keyMetadata = { 61 | id: "PURIFIER", 62 | }; 63 | 64 | await testStore.storeKeys([encryptedKey]); 65 | 66 | const allKeys = await testStore.loadAllKeys(); 67 | 68 | expect(allKeys).toEqual([{ ...encryptedKey, ...keyMetadata }]); 69 | 70 | const removalMetadata = await testStore.removeKey("PURIFIER"); 71 | 72 | expect(removalMetadata).toEqual(keyMetadata); 73 | 74 | const noKeys = await testStore.loadAllKeys(); 75 | 76 | expect(noKeys).toEqual([]); 77 | }); 78 | 79 | it("passes PluginTesting", (done) => { 80 | testKeyStore(testStore) 81 | .then(() => { 82 | done(); 83 | }) 84 | .catch(done); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /src/plugins/LocalStorageKeyStore.ts: -------------------------------------------------------------------------------- 1 | import { getKeyMetadata } from "../helpers/getKeyMetadata"; 2 | import { EncryptedKey, KeyMetadata, KeyStore } from "../types"; 3 | import { 4 | LocalStorageConfigParams, 5 | LocalStorageFacade, 6 | } from "./LocalStorageFacade"; 7 | 8 | /** 9 | * KeyStore for the Web Storage API : 10 | * https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API. 11 | * Once instantiated and configured, pass it to the `KeyManager` contructor to 12 | * handle the storage of encrypted keys. 13 | * ```js 14 | * const localKeyStore = new KeyManagerPlugins.LocalStorageKeyStore(); 15 | * localKeyStore.configure({ storage: localStorage }); 16 | * const keyManager = new KeyManager({ 17 | * keyStore: localKeyStore 18 | * }); 19 | * ``` 20 | */ 21 | export class LocalStorageKeyStore implements KeyStore { 22 | public name: string; 23 | private keyStore: LocalStorageFacade; 24 | 25 | constructor() { 26 | this.name = "LocalStorageKeyStore"; 27 | this.keyStore = new LocalStorageFacade(); 28 | } 29 | 30 | /** 31 | * The configuration is where the storage engine is set up and configured. 32 | * It must follow the Storage interface : 33 | * https://developer.mozilla.org/en-US/docs/Web/API/Storage). 34 | * In the DOM environment it can be `localStorage` or `sessionStorage`. 35 | * In a Node environment, there are some substitution libraries available, 36 | * *node-localstorage* for instance. 37 | * If not set, the calls to the other methods will fail. 38 | * @param {LocalStorageConfigParams} params A configuration object. 39 | * @param {Storage} params.storage The Storage instance. Required. 40 | * @param {string} [params.prefix] The prefix for the names in the storage. 41 | * @return {Promise} 42 | */ 43 | public configure(params: LocalStorageConfigParams) { 44 | try { 45 | this.keyStore.configure(params); 46 | return Promise.resolve(); 47 | } catch (e) { 48 | return Promise.reject(e); 49 | } 50 | } 51 | 52 | public storeKeys(keys: EncryptedKey[]) { 53 | // We can't store keys if they're already there 54 | const invalidKeys: EncryptedKey[] = keys.filter( 55 | (encryptedKey: EncryptedKey) => this.keyStore.hasKey(encryptedKey.id), 56 | ); 57 | 58 | if (invalidKeys.length) { 59 | return Promise.reject( 60 | `Some keys were already stored in the keystore: ${invalidKeys 61 | .map((k) => k.id) 62 | .join(", ")}`, 63 | ); 64 | } 65 | 66 | const keysMetadata = keys.map((encryptedKey: EncryptedKey) => { 67 | this.keyStore.setKey(encryptedKey.id, encryptedKey); 68 | return getKeyMetadata(encryptedKey); 69 | }); 70 | 71 | return Promise.resolve(keysMetadata); 72 | } 73 | 74 | public updateKeys(keys: EncryptedKey[]) { 75 | // we can't update keys if they're already stored 76 | const invalidKeys: EncryptedKey[] = keys.filter( 77 | (encryptedKey: EncryptedKey) => !this.keyStore.hasKey(encryptedKey.id), 78 | ); 79 | 80 | if (invalidKeys.length) { 81 | return Promise.reject( 82 | `Some keys couldn't be found in the keystore: ${invalidKeys 83 | .map((k) => k.id) 84 | .join(", ")}`, 85 | ); 86 | } 87 | 88 | const keysMetadata = keys.map((encryptedKey: EncryptedKey) => { 89 | this.keyStore.setKey(encryptedKey.id, encryptedKey); 90 | return getKeyMetadata(encryptedKey); 91 | }); 92 | 93 | return Promise.resolve(keysMetadata); 94 | } 95 | 96 | public loadKey(id: string) { 97 | const key = this.keyStore.getKey(id); 98 | if (!key) { 99 | return Promise.reject(id); 100 | } 101 | return Promise.resolve(key); 102 | } 103 | 104 | public removeKey(id: string) { 105 | if (!this.keyStore.hasKey(id)) { 106 | return Promise.reject(id); 107 | } 108 | 109 | const metadata: KeyMetadata = getKeyMetadata(this.keyStore.getKey(id)); 110 | this.keyStore.removeKey(id); 111 | 112 | return Promise.resolve(metadata); 113 | } 114 | 115 | public loadAllKeys() { 116 | return Promise.resolve(this.keyStore.getAllKeys()); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/plugins/MemoryKeyStore.test.ts: -------------------------------------------------------------------------------- 1 | import sinon from "sinon"; 2 | 3 | import { testKeyStore } from "../PluginTesting"; 4 | import { EncryptedKey } from "../types"; 5 | import { MemoryKeyStore } from "./MemoryKeyStore"; 6 | 7 | // tslint:disable-next-line 8 | describe("MemoryKeyStore", function() { 9 | let clock: sinon.SinonFakeTimers; 10 | 11 | beforeEach(() => { 12 | clock = sinon.useFakeTimers(666); 13 | }); 14 | 15 | afterEach(() => { 16 | clock.restore(); 17 | }); 18 | 19 | it("properly stores keys", async () => { 20 | const testStore = new MemoryKeyStore(); 21 | 22 | const encryptedKey: EncryptedKey = { 23 | id: "PURIFIER", 24 | encryptedBlob: "BLOB", 25 | encrypterName: "Test", 26 | salt: "SLFKJSDLKFJLSKDJFLKSJD", 27 | }; 28 | 29 | const keyMetadata = { 30 | id: "PURIFIER", 31 | }; 32 | 33 | const testMetadata = await testStore.storeKeys([encryptedKey]); 34 | 35 | expect(testMetadata).toEqual([keyMetadata]); 36 | 37 | const allKeys = await testStore.loadAllKeys(); 38 | 39 | expect(allKeys).toEqual([{ ...encryptedKey, ...keyMetadata }]); 40 | }); 41 | 42 | it("properly deletes keys", async () => { 43 | const testStore = new MemoryKeyStore(); 44 | 45 | const encryptedKey: EncryptedKey = { 46 | id: "PURIFIER", 47 | encrypterName: "Test", 48 | encryptedBlob: "BLOB", 49 | salt: "SLFKJSDLKFJLSKDJFLKSJD", 50 | }; 51 | 52 | const keyMetadata = { 53 | id: "PURIFIER", 54 | }; 55 | 56 | await testStore.storeKeys([encryptedKey]); 57 | 58 | const allKeys = await testStore.loadAllKeys(); 59 | 60 | expect(allKeys).toEqual([{ ...encryptedKey, ...keyMetadata }]); 61 | 62 | const removalMetadata = await testStore.removeKey("PURIFIER"); 63 | 64 | expect(removalMetadata).toEqual(keyMetadata); 65 | 66 | const noKeys = await testStore.loadAllKeys(); 67 | 68 | expect(noKeys).toEqual([]); 69 | }); 70 | 71 | it("passes PluginTesting", (done) => { 72 | testKeyStore(new MemoryKeyStore()) 73 | .then(() => { 74 | done(); 75 | }) 76 | .catch(done); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/plugins/MemoryKeyStore.ts: -------------------------------------------------------------------------------- 1 | import { getKeyMetadata } from "../helpers/getKeyMetadata"; 2 | import { EncryptedKey, KeyMetadata, KeyStore } from "../types"; 3 | 4 | interface MemoryStorer { 5 | [id: string]: EncryptedKey; 6 | } 7 | 8 | export class MemoryKeyStore implements KeyStore { 9 | public name: string; 10 | private keyStore: MemoryStorer; 11 | 12 | constructor() { 13 | this.name = "MemoryKeyStore"; 14 | this.keyStore = {}; 15 | } 16 | 17 | public configure() { 18 | return Promise.resolve(); 19 | } 20 | 21 | public storeKeys(keys: EncryptedKey[]) { 22 | // We can't store keys if they're already there 23 | const invalidKeys: EncryptedKey[] = keys.filter( 24 | (encryptedKey: EncryptedKey) => !!this.keyStore[encryptedKey.id], 25 | ); 26 | 27 | if (invalidKeys.length) { 28 | return Promise.reject( 29 | `Some keys were already stored in the keystore: ${invalidKeys 30 | .map((k) => k.id) 31 | .join(", ")}`, 32 | ); 33 | } 34 | 35 | const keysMetadata = keys.map((encryptedKey: EncryptedKey) => { 36 | this.keyStore[encryptedKey.id] = { 37 | ...encryptedKey, 38 | }; 39 | 40 | return getKeyMetadata(this.keyStore[encryptedKey.id]); 41 | }); 42 | 43 | return Promise.resolve(keysMetadata); 44 | } 45 | 46 | public updateKeys(keys: EncryptedKey[]) { 47 | // we can't update keys if they're already stored 48 | const invalidKeys: EncryptedKey[] = keys.filter( 49 | (encryptedKey: EncryptedKey) => !this.keyStore[encryptedKey.id], 50 | ); 51 | 52 | if (invalidKeys.length) { 53 | return Promise.reject( 54 | `Some keys couldn't be found in the keystore: ${invalidKeys 55 | .map((k) => k.id) 56 | .join(", ")}`, 57 | ); 58 | } 59 | 60 | const keysMetadata = keys.map((encryptedKey: EncryptedKey) => { 61 | const id = encryptedKey.id; 62 | 63 | this.keyStore[id] = { 64 | ...encryptedKey, 65 | }; 66 | 67 | return getKeyMetadata(this.keyStore[id]); 68 | }); 69 | 70 | return Promise.resolve(keysMetadata); 71 | } 72 | 73 | public loadKey(id: string) { 74 | return Promise.resolve(this.keyStore[id]); 75 | } 76 | 77 | public removeKey(id: string) { 78 | if (!this.keyStore[id]) { 79 | return Promise.reject(id); 80 | } 81 | 82 | const metadata: KeyMetadata = getKeyMetadata(this.keyStore[id]); 83 | delete this.keyStore[id]; 84 | 85 | return Promise.resolve(metadata); 86 | } 87 | 88 | public loadAllKeys() { 89 | return Promise.resolve( 90 | Object.values(this.keyStore).map((item: EncryptedKey) => item), 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/plugins/ScryptEncrypter.test.ts: -------------------------------------------------------------------------------- 1 | import { KeyType } from "../constants/keys"; 2 | import { testEncrypter } from "../PluginTesting"; 3 | import { ScryptEncrypter } from "./ScryptEncrypter"; 4 | 5 | test("encrypts and decrypts a key", async () => { 6 | const key = { 7 | type: KeyType.plaintextKey, 8 | publicKey: "AVACYN", 9 | privateKey: "ARCHANGEL", 10 | id: "PURIFIER", 11 | path: "PATH", 12 | extra: "EXTRA", 13 | }; 14 | 15 | const password = "This is a really cool password and is good"; 16 | 17 | const encryptedKey = await ScryptEncrypter.encryptKey({ 18 | key, 19 | password, 20 | }); 21 | 22 | expect(encryptedKey).toBeTruthy(); 23 | expect(encryptedKey.encryptedBlob).toBeTruthy(); 24 | expect(encryptedKey.encryptedBlob).not.toEqual(key.privateKey); 25 | 26 | const decryptedKey = await ScryptEncrypter.decryptKey({ 27 | encryptedKey, 28 | password, 29 | }); 30 | 31 | expect(decryptedKey.privateKey).not.toEqual(encryptedKey.encryptedBlob); 32 | expect(decryptedKey).toEqual(key); 33 | }); 34 | 35 | it("passes PluginTesting", async () => { 36 | expect(await testEncrypter(ScryptEncrypter)).toEqual(true); 37 | }); 38 | -------------------------------------------------------------------------------- /src/plugins/ScryptEncrypter.ts: -------------------------------------------------------------------------------- 1 | import { decrypt, encrypt } from "../helpers/ScryptEncryption"; 2 | import { 3 | DecryptParams, 4 | EncryptedKey, 5 | Encrypter, 6 | EncryptParams, 7 | } from "../types"; 8 | 9 | const NAME = "ScryptEncrypter"; 10 | 11 | /** 12 | * Encrypt keys with scrypt, as they are on StellarX.com. 13 | */ 14 | export const ScryptEncrypter: Encrypter = { 15 | name: NAME, 16 | async encryptKey(params: EncryptParams): Promise { 17 | const { key, password } = params; 18 | const { privateKey, path, extra, publicKey, type, ...props } = key; 19 | 20 | const { encryptedPhrase, salt } = await encrypt({ 21 | password, 22 | phrase: JSON.stringify({ privateKey, path, extra, publicKey, type }), 23 | }); 24 | 25 | return { 26 | ...props, 27 | encryptedBlob: encryptedPhrase, 28 | encrypterName: NAME, 29 | salt, 30 | }; 31 | }, 32 | 33 | async decryptKey(params: DecryptParams) { 34 | const { encryptedKey, password } = params; 35 | const { encrypterName, salt, encryptedBlob, ...props } = encryptedKey; 36 | 37 | const data = JSON.parse( 38 | await decrypt({ phrase: encryptedBlob, salt, password }), 39 | ); 40 | 41 | return { 42 | ...props, 43 | ...data, 44 | }; 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /src/sep8/ApprovalProvider.ts: -------------------------------------------------------------------------------- 1 | import { FeeBumpTransaction, Transaction } from "@stellar/stellar-sdk"; 2 | import { ApprovalResponseStatus } from "../constants/sep8"; 3 | import { 4 | ApprovalResponse, 5 | PostActionUrlRequest, 6 | PostActionUrlResponse, 7 | } from "../types/sep8"; 8 | 9 | export class ApprovalProvider { 10 | public approvalServer: string; 11 | 12 | constructor(approvalServer: string) { 13 | if (!approvalServer) { 14 | throw new Error("Required parameter 'approvalServer' missing!"); 15 | } 16 | 17 | this.approvalServer = approvalServer.replace(/\/$/, ""); 18 | } 19 | 20 | public async approve( 21 | transaction: Transaction | FeeBumpTransaction, 22 | contentType: 23 | | "application/json" 24 | | "application/x-www-form-urlencoded" = "application/json", 25 | ): Promise { 26 | if (!transaction.signatures.length) { 27 | throw new Error( 28 | "At least one signature is required before submitting for approval.", 29 | ); 30 | } 31 | 32 | const param = { 33 | tx: transaction 34 | .toEnvelope() 35 | .toXDR() 36 | .toString("base64"), 37 | }; 38 | 39 | let response; 40 | if (contentType === "application/json") { 41 | response = await fetch(this.approvalServer, { 42 | method: "POST", 43 | body: JSON.stringify(param), 44 | headers: { 45 | "Content-Type": "application/json", 46 | }, 47 | }); 48 | } else { 49 | const urlParam = new URLSearchParams(param); 50 | response = await fetch(this.approvalServer, { 51 | method: "POST", 52 | body: urlParam.toString(), 53 | headers: { 54 | "Content-Type": "application/x-www-form-urlencoded", 55 | }, 56 | }); 57 | } 58 | 59 | if (!response.ok) { 60 | const responseText = await response.text(); 61 | throw new Error( 62 | `Error sending base64-encoded transaction to ${ 63 | this.approvalServer 64 | }: error code ${response.status}, status text: "${responseText}"`, 65 | ); 66 | } 67 | 68 | const responseOKText = await response.text(); 69 | let res; 70 | try { 71 | res = JSON.parse(responseOKText) as ApprovalResponse; 72 | } catch (e) { 73 | throw new Error( 74 | `Error parsing the approval server response as JSON: ${responseOKText}`, 75 | ); 76 | } 77 | 78 | const acceptedStatuses = [ 79 | ApprovalResponseStatus.success, 80 | ApprovalResponseStatus.revised, 81 | ApprovalResponseStatus.pending, 82 | ApprovalResponseStatus.actionRequired, 83 | ApprovalResponseStatus.rejected, 84 | ]; 85 | if (!acceptedStatuses.includes(res.status)) { 86 | throw new Error(`Approval server returned unknown status: ${res.status}`); 87 | } 88 | return res; 89 | } 90 | 91 | public async postActionUrl( 92 | params: PostActionUrlRequest, 93 | ): Promise { 94 | if (!params.action_url) { 95 | throw new Error("Required field 'action_url' missing!"); 96 | } 97 | if (!Object.keys(params.field_value_map).length) { 98 | throw new Error("Required field 'field_value_map' missing!"); 99 | } 100 | 101 | const response = await fetch(params.action_url, { 102 | method: "POST", 103 | body: JSON.stringify(params.field_value_map), 104 | headers: { 105 | "Content-Type": "application/json", 106 | }, 107 | }); 108 | 109 | if (!response.ok) { 110 | const responseText = await response.text(); 111 | throw new Error( 112 | `Error sending POST request to ${params.action_url}: error code ${ 113 | response.status 114 | }, status text: "${responseText}"`, 115 | ); 116 | } 117 | 118 | const responseOKText = await response.text(); 119 | try { 120 | return JSON.parse(responseOKText) as PostActionUrlResponse; 121 | } catch (e) { 122 | throw new Error(`Error parsing the response as JSON: ${responseOKText}`); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/sep8/getApprovalServerUrl.test.ts: -------------------------------------------------------------------------------- 1 | import { Config } from "@stellar/stellar-sdk"; 2 | import axios from "axios"; 3 | import sinon from "sinon"; 4 | import { getApprovalServerUrl } from "./getApprovalServerUrl"; 5 | 6 | describe("getApprovalServerUrl", () => { 7 | let axiosMock: sinon.SinonMock; 8 | 9 | beforeEach(() => { 10 | axiosMock = sinon.mock(axios); 11 | Config.setDefault(); 12 | }); 13 | 14 | afterEach(() => { 15 | axiosMock.verify(); 16 | axiosMock.restore(); 17 | }); 18 | 19 | test("Issuer's Home Domain missing", async () => { 20 | try { 21 | // @ts-ignore 22 | const res = await getApprovalServerUrl({ 23 | asset_code: "USD", 24 | asset_issuer: 25 | "GDBMMVJKWGT2N6HZ2BGMFHKODASVFYIHL2VS3RUTB3B3QES2R6YFXGQW", 26 | }); 27 | expect("This test failed").toBe(null); 28 | } catch (e) { 29 | expect((e as any).toString()).toMatch( 30 | `Error: Issuer's home domain is missing`, 31 | ); 32 | } 33 | }); 34 | 35 | test("stellar.toml CURRENCIES missing", async () => { 36 | const homeDomain = "example.com"; 37 | axiosMock 38 | .expects("get") 39 | .withArgs(sinon.match(`https://${homeDomain}/.well-known/stellar.toml`)) 40 | .returns( 41 | Promise.resolve({ 42 | data: "", 43 | }), 44 | ); 45 | 46 | try { 47 | // @ts-ignore 48 | const res = await getApprovalServerUrl({ 49 | asset_code: "USD", 50 | asset_issuer: 51 | "GDBMMVJKWGT2N6HZ2BGMFHKODASVFYIHL2VS3RUTB3B3QES2R6YFXGQW", 52 | home_domain: homeDomain, 53 | }); 54 | expect("This test failed").toBe(null); 55 | } catch (e) { 56 | expect((e as any).toString()).toMatch( 57 | `Error: stellar.toml at ${homeDomain} does not contain CURRENCIES` + 58 | ` field`, 59 | ); 60 | } 61 | }); 62 | 63 | test("stellar.toml approval_server missing", async () => { 64 | const homeDomain = "example.com"; 65 | axiosMock 66 | .expects("get") 67 | .withArgs(sinon.match(`https://${homeDomain}/.well-known/stellar.toml`)) 68 | .returns( 69 | Promise.resolve({ 70 | data: ` 71 | [[CURRENCIES]] 72 | code = "USD" 73 | issuer = "GDBMMVJKWGT2N6HZ2BGMFHKODASVFYIHL2VS3RUTB3B3QES2R6YFXGQW" 74 | `, 75 | }), 76 | ); 77 | 78 | try { 79 | // @ts-ignore 80 | const res = await getApprovalServerUrl({ 81 | asset_code: "USD", 82 | asset_issuer: 83 | "GDBMMVJKWGT2N6HZ2BGMFHKODASVFYIHL2VS3RUTB3B3QES2R6YFXGQW", 84 | home_domain: homeDomain, 85 | }); 86 | expect("This test failed").toBe(null); 87 | } catch (e) { 88 | expect((e as any).toString()).toMatch( 89 | `Error: stellar.toml at ${homeDomain} does not contain` + 90 | ` approval_server information for this asset`, 91 | ); 92 | } 93 | }); 94 | 95 | test("stellar.toml asset not found", async () => { 96 | const homeDomain = "example.com"; 97 | axiosMock 98 | .expects("get") 99 | .withArgs(sinon.match(`https://${homeDomain}/.well-known/stellar.toml`)) 100 | .returns( 101 | Promise.resolve({ 102 | data: ` 103 | [[CURRENCIES]] 104 | code = "USD" 105 | issuer = "GDBMMVJKWGT2N6HZ2BGMFHKODASVFYIHL2VS3RUTB3B3QES2R6YFXGQW" 106 | `, 107 | }), 108 | ); 109 | 110 | try { 111 | // @ts-ignore 112 | const res = await getApprovalServerUrl({ 113 | asset_code: "EUR", 114 | asset_issuer: 115 | "GDBMMVJKWGT2N6HZ2BGMFHKODASVFYIHL2VS3RUTB3B3QES2R6YFXGQW", 116 | home_domain: homeDomain, 117 | }); 118 | expect("This test failed").toBe(null); 119 | } catch (e) { 120 | expect((e as any).toString()).toMatch( 121 | `Error: CURRENCY EUR:` + 122 | `GDBMMVJKWGT2N6HZ2BGMFHKODASVFYIHL2VS3RUTB3B3QES2R6YFXGQW` + 123 | ` not found on stellar.toml at ${homeDomain}`, 124 | ); 125 | } 126 | }); 127 | 128 | test("approval server URL is returned", async () => { 129 | const homeDomain = "example.com"; 130 | axiosMock 131 | .expects("get") 132 | .withArgs(sinon.match(`https://${homeDomain}/.well-known/stellar.toml`)) 133 | .returns( 134 | Promise.resolve({ 135 | data: ` 136 | [[CURRENCIES]] 137 | code = "USD" 138 | issuer = "GDBMMVJKWGT2N6HZ2BGMFHKODASVFYIHL2VS3RUTB3B3QES2R6YFXGQW" 139 | approval_server = "https://example.com/approve" 140 | `, 141 | }), 142 | ); 143 | 144 | try { 145 | const res = await getApprovalServerUrl({ 146 | asset_code: "USD", 147 | asset_issuer: 148 | "GDBMMVJKWGT2N6HZ2BGMFHKODASVFYIHL2VS3RUTB3B3QES2R6YFXGQW", 149 | home_domain: homeDomain, 150 | }); 151 | expect(res).toEqual("https://example.com/approve"); 152 | } catch (e) { 153 | expect(e).toBe(null); 154 | } 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /src/sep8/getApprovalServerUrl.ts: -------------------------------------------------------------------------------- 1 | import { StellarToml } from "@stellar/stellar-sdk"; 2 | import { RegulatedAssetInfo } from "../types/sep8"; 3 | 4 | export async function getApprovalServerUrl( 5 | param: RegulatedAssetInfo, 6 | opts: StellarToml.Api.StellarTomlResolveOptions = {}, 7 | ): Promise { 8 | if (!param.home_domain) { 9 | throw new Error(`Issuer's home domain is missing`); 10 | } 11 | 12 | const tomlObject = await StellarToml.Resolver.resolve( 13 | param.home_domain, 14 | opts, 15 | ); 16 | if (!tomlObject.CURRENCIES) { 17 | throw new Error( 18 | `stellar.toml at ${param.home_domain} does not contain CURRENCIES field`, 19 | ); 20 | } 21 | 22 | for (const ast of tomlObject.CURRENCIES) { 23 | if (ast.code === param.asset_code && ast.issuer === param.asset_issuer) { 24 | if (!ast.approval_server) { 25 | throw new Error( 26 | `stellar.toml at ${ 27 | param.home_domain 28 | } does not contain approval_server information for this asset`, 29 | ); 30 | } 31 | 32 | return ast.approval_server; 33 | } 34 | } 35 | 36 | throw new Error( 37 | `CURRENCY ${param.asset_code}:${ 38 | param.asset_issuer 39 | } not found on stellar.toml at ${param.home_domain}`, 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/sep8/getRegulatedAssetsInTx.ts: -------------------------------------------------------------------------------- 1 | import { Asset, Horizon, Operation, Transaction } from "@stellar/stellar-sdk"; 2 | import { RegulatedAssetInfo } from "../types/sep8"; 3 | 4 | export async function getRegulatedAssetsInTx( 5 | tx: Transaction, 6 | horizonUrl: string, 7 | ): Promise { 8 | const res: RegulatedAssetInfo[] = []; 9 | const server = new Horizon.Server(horizonUrl); 10 | for (const op of tx.operations) { 11 | if (!isOpMovingAsset(op)) { 12 | continue; 13 | } 14 | const assets = await getAssetsFromOp(op, server); 15 | if (!assets.length) { 16 | throw new Error(`Couldn't get asset(s) in operation ${op.type}`); 17 | } 18 | 19 | for (const ast of assets) { 20 | try { 21 | const accountDetail = await server.loadAccount(ast.getIssuer()); 22 | if ( 23 | accountDetail.flags.auth_required && 24 | accountDetail.flags.auth_revocable 25 | ) { 26 | res.push({ 27 | asset_code: ast.getCode(), 28 | asset_issuer: ast.getIssuer(), 29 | home_domain: accountDetail.home_domain, 30 | }); 31 | } 32 | } catch (e) { 33 | throw new Error( 34 | `Couldn't get asset issuer information ${ast.getIssuer()}` + 35 | ` in operation ${ 36 | op.type 37 | } from ${horizonUrl}: ${(e as any).toString()}`, 38 | ); 39 | } 40 | } 41 | } 42 | 43 | return res; 44 | } 45 | 46 | function isOpMovingAsset(op: Operation): boolean { 47 | return [ 48 | "payment", 49 | "pathPaymentStrictReceive", 50 | "pathPaymentStrictSend", 51 | "createPassiveSellOffer", 52 | "manageSellOffer", 53 | "manageBuyOffer", 54 | "createClaimableBalance", 55 | "claimClaimableBalance", 56 | ].includes(op.type); 57 | } 58 | 59 | async function getAssetsFromOp( 60 | op: Operation, 61 | server: Horizon.Server, 62 | ): Promise { 63 | switch (op.type) { 64 | case "payment": 65 | return [op.asset]; 66 | case "pathPaymentStrictReceive": 67 | return [op.sendAsset, op.destAsset]; 68 | case "pathPaymentStrictSend": 69 | return [op.sendAsset, op.destAsset]; 70 | case "createPassiveSellOffer": 71 | return [op.selling, op.buying]; 72 | case "manageSellOffer": 73 | return [op.selling, op.buying]; 74 | case "manageBuyOffer": 75 | return [op.selling, op.buying]; 76 | case "createClaimableBalance": 77 | return [op.asset]; 78 | case "claimClaimableBalance": 79 | try { 80 | const cBalance = await server 81 | .claimableBalances() 82 | .claimableBalance(op.balanceId) 83 | .call(); 84 | 85 | const [code, issuer] = cBalance.asset.split(":", 2); 86 | return [new Asset(code, issuer)]; 87 | } catch (e) { 88 | throw new Error( 89 | `Error getting claimbable balance with id ${ 90 | op.balanceId 91 | } from: ${server.serverURL.toString()}`, 92 | ); 93 | } 94 | default: 95 | return []; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/testUtils.ts: -------------------------------------------------------------------------------- 1 | export interface Obj { 2 | [key: string]: any; 3 | } 4 | 5 | export function parseRecord(json: Obj): Obj { 6 | if (!json._links) { 7 | return json; 8 | } 9 | 10 | Object.keys(json._links).forEach((key: string) => { 11 | // If the key with the link name already exists, create a copy 12 | if (typeof json[key] !== "undefined") { 13 | json[`${key}_attr`] = json[key]; 14 | } 15 | 16 | json[key] = () => null; 17 | }); 18 | return json; 19 | } 20 | 21 | export function parseCollection(json: Obj): Obj { 22 | for (let i = 0; i < json._embedded.records.length; i += 1) { 23 | json._embedded.records[i] = parseRecord(json._embedded.records[i]); 24 | } 25 | return { 26 | records: json._embedded.records, 27 | next: () => null, 28 | prev: () => null, 29 | }; 30 | } 31 | 32 | export function parseResponse(json: Obj): Obj { 33 | if (json._embedded && json._embedded.records) { 34 | return parseCollection(json); 35 | } 36 | 37 | return parseRecord(json); 38 | } 39 | -------------------------------------------------------------------------------- /src/transfers/fetchKycInBrowser.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DepositRequest, 3 | FetchKycInBrowserParams, 4 | KycStatus, 5 | WithdrawRequest, 6 | } from "../types"; 7 | 8 | import { getKycUrl } from "./getKycUrl"; 9 | 10 | export function fetchKycInBrowser( 11 | params: FetchKycInBrowserParams, 12 | ): Promise { 13 | const { response, window: windowContext } = params; 14 | const { origin } = new URL(response.url); 15 | return new Promise((resolve, reject) => { 16 | const handleMessage = (e: MessageEvent) => { 17 | if (e.origin !== origin) { 18 | // React devtools appear to pump out tons and tons of these events, 19 | // this filters them out. 20 | return; 21 | } 22 | 23 | windowContext.removeEventListener("message", handleMessage); 24 | windowContext.close(); 25 | if (e.data.status === "success") { 26 | resolve(e.data); 27 | } else { 28 | // TODO: clarify error sources 29 | reject(e.data); 30 | } 31 | }; 32 | 33 | windowContext.addEventListener("message", handleMessage); 34 | windowContext.location.href = getKycUrl({ 35 | response, 36 | callback_url: "postMessage", 37 | }); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /src/transfers/getKycUrl.test.ts: -------------------------------------------------------------------------------- 1 | import { TransferResponseType } from "../constants/transfers"; 2 | import { getKycUrl, KycUrlParams } from "./getKycUrl"; 3 | 4 | function getParams(url: string, callback_url?: string): KycUrlParams { 5 | return { 6 | request: { 7 | amount: "2000", 8 | asset_code: "test", 9 | }, 10 | response: { 11 | type: TransferResponseType.interactive_customer_info_needed, 12 | url, 13 | id: "116", 14 | interactive_deposit: true, 15 | }, 16 | callback_url, 17 | }; 18 | } 19 | 20 | test("works properly on simple urls", () => { 21 | expect(getKycUrl(getParams("https://www.google.com", "postMessage"))).toEqual( 22 | "https://www.google.com/?callback=postMessage", 23 | ); 24 | }); 25 | 26 | test("works properly on urls with directories", () => { 27 | expect( 28 | getKycUrl(getParams("https://www.google.com/mail", "postMessage")), 29 | ).toEqual("https://www.google.com/mail?callback=postMessage"); 30 | }); 31 | 32 | test("works properly on urls with querystrings", () => { 33 | expect( 34 | getKycUrl(getParams("https://www.google.com?good=true", "postMessage")), 35 | ).toEqual("https://www.google.com/?good=true&callback=postMessage"); 36 | }); 37 | 38 | test("works properly on urls with querystrings and hashes", () => { 39 | expect( 40 | getKycUrl( 41 | getParams("https://www.google.com/?good=true#page=1", "postMessage"), 42 | ), 43 | ).toEqual("https://www.google.com/?good=true&callback=postMessage#page=1"); 44 | }); 45 | 46 | test("works properly on urls with everything", () => { 47 | expect( 48 | getKycUrl( 49 | getParams( 50 | "https://www.google.com/mail/one?good=true#page=1", 51 | "postMessage", 52 | ), 53 | ), 54 | ).toEqual( 55 | "https://www.google.com/mail/one?good=true&callback=postMessage#page=1", 56 | ); 57 | }); 58 | 59 | test("callback not needed", () => { 60 | expect( 61 | getKycUrl(getParams("https://www.google.com/mail/one?good=true#page=1")), 62 | ).toEqual("https://www.google.com/mail/one?good=true#page=1"); 63 | }); 64 | -------------------------------------------------------------------------------- /src/transfers/getKycUrl.ts: -------------------------------------------------------------------------------- 1 | import queryString from "query-string"; 2 | 3 | import { 4 | DepositRequest, 5 | InteractiveKycNeededResponse, 6 | WithdrawRequest, 7 | } from "../types"; 8 | 9 | interface BaseParams { 10 | response: InteractiveKycNeededResponse; 11 | } 12 | 13 | interface PostMessageParams extends BaseParams { 14 | callback_url: "postMessage"; 15 | } 16 | interface CallbackUrlParams extends BaseParams { 17 | callback_url: string; 18 | request: DepositRequest | WithdrawRequest; 19 | } 20 | 21 | export type KycUrlParams = BaseParams | PostMessageParams | CallbackUrlParams; 22 | 23 | /** 24 | * `getKycUrl` takes in the original request object, a response object, and a 25 | * URL that the anchor should redirect to after KYC is complete. 26 | * 27 | * For ease of development, the original request is provided as a querystring 28 | * argument to the callback URL. This reduces how much state the application 29 | * must keep track of, since the deposit or withdrawal needs to be retried once 30 | * KYC is approved. 31 | * 32 | * ```js 33 | * if (depositResponse === TransferResponseType.interactiveKyc) { 34 | * const kycRedirect = getKycUrl({ 35 | * result: withdrawResult, 36 | * callback_url, 37 | * }); 38 | * } 39 | * ``` 40 | * 41 | * On e.g. react native, the client will have to open a webview manually 42 | * and pass a callback URL that the app has "claimed." This is very 43 | * similar to e.g. OAuth flows. 44 | * https://www.oauth.com/oauth2-servers/redirect-uris/redirect-uris-native-apps/ 45 | * 46 | * @param {KycUrlParams} params An object with 3 properties 47 | * @param {DepositRequest | WithdrawRequest} params.request The original request 48 | * that needs KYC information. 49 | * @param {InteractiveKycNeededResponse} params.response The complete response. 50 | * @param {string} params.callback_url A url that the anchor should send 51 | * information to once KYC information has been received. 52 | * @returns {string} A URL to open so users can complete their KYC information. 53 | */ 54 | export function getKycUrl(params: KycUrlParams) { 55 | // break apart and re-produce the URL 56 | const { origin, pathname, search, hash } = new URL(params.response.url); 57 | 58 | let callback = ""; 59 | 60 | const { callback_url, request } = params as CallbackUrlParams; 61 | 62 | if (callback_url === "postMessage") { 63 | callback = `${search ? "&" : "?"}callback=postMessage`; 64 | } else if (callback_url && request) { 65 | // If the callback arg is a URL, add the original request to it as a 66 | // querystring argument. 67 | const url = new URL(callback_url); 68 | const newParams = { ...queryString.parse(url.search) }; 69 | newParams.request = encodeURIComponent(JSON.stringify(request)); 70 | url.search = queryString.stringify(newParams); 71 | callback = `${search ? "&" : "?"}callback=${encodeURIComponent( 72 | url.toString(), 73 | )}`; 74 | } 75 | 76 | return `${origin}${pathname}${search}${callback}${hash}`; 77 | } 78 | -------------------------------------------------------------------------------- /src/transfers/index.ts: -------------------------------------------------------------------------------- 1 | export { DepositProvider } from "./DepositProvider"; 2 | export { WithdrawProvider } from "./WithdrawProvider"; 3 | export { getKycUrl } from "./getKycUrl"; 4 | -------------------------------------------------------------------------------- /src/transfers/parseInfo.test.ts: -------------------------------------------------------------------------------- 1 | import { parseInfo } from "./parseInfo"; 2 | 3 | import { 4 | AnchorUSDTransferInfo, 5 | ApayTransferInfo, 6 | } from "../fixtures/TransferInfoResponse"; 7 | 8 | it("AnchorUSD: runs without error", () => { 9 | parseInfo(AnchorUSDTransferInfo); 10 | }); 11 | 12 | it("Apay: runs without error", () => { 13 | parseInfo(ApayTransferInfo); 14 | }); 15 | -------------------------------------------------------------------------------- /src/transfers/parseInfo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DepositAssetInfoMap, 3 | Fee, 4 | Field, 5 | RawField, 6 | RawInfoResponse, 7 | RawType, 8 | SimpleFee, 9 | WithdrawAssetInfoMap, 10 | } from "../types"; 11 | 12 | function isValidInfoResponse(obj: any): obj is RawInfoResponse { 13 | return ( 14 | (obj as RawInfoResponse).withdraw !== undefined && 15 | (obj as RawInfoResponse).deposit !== undefined 16 | ); 17 | } 18 | 19 | export function parseInfo(info: any) { 20 | if (!isValidInfoResponse(info)) { 21 | throw new Error("The endpoint didn't return a valid info response!"); 22 | } 23 | const { fee, transactions, transaction } = info as RawInfoResponse; 24 | return { 25 | withdraw: parseWithdraw(info), 26 | deposit: parseDeposit(info), 27 | fee, 28 | transactions, 29 | transaction, 30 | }; 31 | } 32 | 33 | const parseFee = ( 34 | { 35 | fee_fixed, 36 | fee_percent, 37 | }: { 38 | fee_fixed: number; 39 | fee_percent: number; 40 | }, 41 | feeEnabled: boolean, 42 | ): Fee => { 43 | if ( 44 | (fee_fixed !== undefined && Number(fee_fixed) >= 0) || 45 | (fee_percent !== undefined && Number(fee_percent) >= 0) 46 | ) { 47 | return { 48 | type: "simple", 49 | fixed: fee_fixed, 50 | percent: fee_percent, 51 | } as SimpleFee; 52 | } else { 53 | return { 54 | type: feeEnabled ? "complex" : "none", 55 | } as Fee; 56 | } 57 | }; 58 | 59 | function parseType([typeName, type]: [string, RawType]) { 60 | return { 61 | name: typeName, 62 | fields: Object.entries(type.fields || {}).map(parseField), 63 | }; 64 | } 65 | 66 | type FieldEntry = [string, RawField]; 67 | 68 | function parseField([fieldName, field]: FieldEntry): Field { 69 | return { 70 | ...field, 71 | name: fieldName, 72 | }; 73 | } 74 | 75 | export function parseWithdraw(info: RawInfoResponse): WithdrawAssetInfoMap { 76 | return Object.entries(info.withdraw).reduce( 77 | (accum, [asset_code, entry]) => { 78 | const fee = parseFee(entry, !!(info.fee && info.fee.enabled)); 79 | 80 | accum[asset_code] = { 81 | asset_code, 82 | fee, 83 | min_amount: entry.min_amount, 84 | max_amount: entry.max_amount, 85 | authentication_required: !!entry.authentication_required, 86 | types: Object.entries(entry.types || {}).map(parseType), 87 | }; 88 | return accum; 89 | }, 90 | {} as WithdrawAssetInfoMap, 91 | ); 92 | } 93 | 94 | export function parseDeposit(info: RawInfoResponse): DepositAssetInfoMap { 95 | return Object.entries(info.deposit).reduce( 96 | (accum, [asset_code, entry]) => { 97 | const fee = parseFee(entry, !!(info.fee && info.fee.enabled)); 98 | 99 | accum[asset_code] = { 100 | asset_code, 101 | fee, 102 | min_amount: entry.min_amount, 103 | max_amount: entry.max_amount, 104 | authentication_required: !!entry.authentication_required, 105 | fields: Object.entries(entry.fields || {}).map(parseField), 106 | }; 107 | return accum; 108 | }, 109 | {} as DepositAssetInfoMap, 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /src/types/@modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module "randombytes"; 2 | -------------------------------------------------------------------------------- /src/types/data.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AssetType, 3 | BadRequestError, 4 | Horizon, 5 | Memo, 6 | MemoType, 7 | NetworkError, 8 | NotFoundError, 9 | } from "@stellar/stellar-sdk"; 10 | import BigNumber from "bignumber.js"; 11 | import { EffectType } from "../constants/data"; 12 | 13 | interface NotFundedError { 14 | isUnfunded: boolean; 15 | } 16 | 17 | export type FetchAccountError = 18 | | BadRequestError 19 | | NetworkError 20 | | (NotFoundError & NotFundedError); 21 | 22 | export type TradeId = string; 23 | export type OfferId = string; 24 | 25 | export interface KeyMap { 26 | [key: string]: any; 27 | } 28 | 29 | export interface Account { 30 | publicKey: string; 31 | } 32 | 33 | export interface Issuer { 34 | key: string; 35 | name?: string; 36 | url?: string; 37 | hostName?: string; 38 | } 39 | 40 | export interface NativeToken { 41 | type: AssetType; 42 | code: string; 43 | } 44 | 45 | export interface AssetToken { 46 | type: AssetType; 47 | code: string; 48 | issuer: Issuer; 49 | anchorAsset?: string; 50 | numAccounts?: BigNumber; 51 | amount?: BigNumber; 52 | bidCount?: BigNumber; 53 | askCount?: BigNumber; 54 | spread?: BigNumber; 55 | } 56 | 57 | export type Token = NativeToken | AssetToken; 58 | 59 | export interface Effect { 60 | id: string; 61 | type?: EffectType; 62 | senderToken: Token; 63 | receiverToken: Token; 64 | senderAccount: Account; 65 | receiverAccount?: Account; 66 | senderAmount: BigNumber; 67 | receiverAmount: BigNumber; 68 | timestamp: number; 69 | } 70 | 71 | export interface ReframedEffect { 72 | id: string; 73 | type?: EffectType; 74 | baseToken: Token; 75 | token: Token; 76 | amount: BigNumber; 77 | price: BigNumber; 78 | sender?: Account; 79 | timestamp: number; 80 | } 81 | 82 | /** 83 | * Trades are framed in terms of the account you used to initiate DataProvider. 84 | * That means that a trade object will say which token was your "payment" in 85 | * the exchange (the token and amount you sent to someone else) and what was 86 | * "incoming". 87 | */ 88 | export interface Trade { 89 | id: string; 90 | 91 | paymentToken: Token; 92 | paymentAmount: BigNumber; 93 | paymentOfferId?: OfferId; 94 | 95 | incomingToken: Token; 96 | incomingAccount: Account; 97 | incomingAmount: BigNumber; 98 | incomingOfferId?: OfferId; 99 | 100 | timestamp: number; 101 | } 102 | 103 | export interface Offer { 104 | id: OfferId; 105 | offerer: Account; 106 | paymentToken: Token; 107 | incomingToken: Token; 108 | incomingTokenPrice: BigNumber; 109 | incomingAmount: BigNumber; 110 | paymentAmount: BigNumber; 111 | initialPaymentAmount: BigNumber; 112 | timestamp: number; 113 | 114 | resultingTrades: TradeId[]; 115 | } 116 | 117 | export interface Payment { 118 | id: string; 119 | isInitialFunding: boolean; 120 | isRecipient: boolean; 121 | 122 | token: Token; 123 | amount: BigNumber; 124 | timestamp: number; 125 | otherAccount: Account; 126 | 127 | sourceToken?: Token; 128 | sourceAmount?: BigNumber; 129 | 130 | transactionId: string; 131 | type: Horizon.HorizonApi.OperationResponseType; 132 | 133 | memo?: Memo | string; 134 | memoType?: MemoType; 135 | 136 | mergedAccount?: Account; 137 | } 138 | 139 | export interface Balance { 140 | token: Token; 141 | 142 | // for non-native tokens, this should be total - sellingLiabilities 143 | // for native, it should also subtract the minimumBalance 144 | available: BigNumber; 145 | total: BigNumber; 146 | buyingLiabilities: BigNumber; 147 | sellingLiabilities: BigNumber; 148 | } 149 | 150 | export interface AssetBalance extends Balance { 151 | token: AssetToken; 152 | sponsor?: string; 153 | } 154 | 155 | export interface NativeBalance extends Balance { 156 | token: NativeToken; 157 | minimumBalance: BigNumber; 158 | } 159 | 160 | export interface BalanceMap { 161 | [key: string]: AssetBalance | NativeBalance; 162 | native: NativeBalance; 163 | } 164 | 165 | export interface AccountDetails { 166 | id: string; 167 | subentryCount: number; 168 | sponsoringCount: number; 169 | sponsoredCount: number; 170 | sponsor?: string; 171 | inflationDestination?: string; 172 | thresholds: Horizon.HorizonApi.AccountThresholds; 173 | signers: Horizon.ServerApi.AccountRecordSigners[]; 174 | flags: Horizon.HorizonApi.Flags; 175 | balances: BalanceMap; 176 | sequenceNumber: string; 177 | } 178 | 179 | export interface CollectionParams { 180 | limit?: number; 181 | order?: "desc" | "asc"; 182 | cursor?: string; 183 | } 184 | 185 | export interface Collection { 186 | next: () => Promise>; 187 | prev: () => Promise>; 188 | records: Record[]; 189 | } 190 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./data"; 2 | export * from "./keys"; 3 | export * from "./transfers"; 4 | export * from "./watchers"; 5 | export * from "./sep8"; 6 | -------------------------------------------------------------------------------- /src/types/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@ledgerhq/hw-app-str"; 2 | declare module "scrypt-async"; 3 | declare module "jest-mock-random"; 4 | // @types/trezor-connect doesn't have stellarSignTransaction() 5 | declare module "trezor-connect"; 6 | declare module "@albedo-link/intent"; 7 | -------------------------------------------------------------------------------- /src/types/sep8.ts: -------------------------------------------------------------------------------- 1 | import { ActionResult, ApprovalResponseStatus } from "../constants/sep8"; 2 | 3 | export interface ApprovalResponse { 4 | status: ApprovalResponseStatus; 5 | } 6 | 7 | export interface TransactionApproved { 8 | status: ApprovalResponseStatus.success; 9 | tx: string; 10 | message?: string; 11 | } 12 | 13 | export interface TransactionRevised { 14 | status: ApprovalResponseStatus.revised; 15 | tx: string; 16 | message: string; 17 | } 18 | 19 | export interface PendingApproval { 20 | status: ApprovalResponseStatus.pending; 21 | timeout: number; 22 | message?: string; 23 | } 24 | 25 | export interface ActionRequired { 26 | status: ApprovalResponseStatus.actionRequired; 27 | message: string; 28 | action_url: string; 29 | action_method?: string; 30 | action_fields?: string[]; 31 | } 32 | 33 | export interface TransactionRejected { 34 | status: ApprovalResponseStatus.rejected; 35 | error: string; 36 | } 37 | 38 | export interface PostActionUrlRequest { 39 | action_url: string; 40 | field_value_map: { [key: string]: any }; 41 | } 42 | 43 | export interface PostActionUrlResponse { 44 | result: ActionResult; 45 | next_url?: string; 46 | message?: string; 47 | } 48 | 49 | export interface RegulatedAssetInfo { 50 | asset_code: string; 51 | asset_issuer: string; 52 | home_domain?: string; 53 | } 54 | -------------------------------------------------------------------------------- /src/types/watchers.ts: -------------------------------------------------------------------------------- 1 | export interface WatcherParams { 2 | onMessage: (payload: T) => void; 3 | onError: (error: any) => void; 4 | } 5 | 6 | export type WatcherRefreshFunction = () => void; 7 | export type WatcherStopFunction = () => void; 8 | 9 | export interface WatcherResponse { 10 | refresh: WatcherRefreshFunction; 11 | stop: WatcherStopFunction; 12 | } 13 | -------------------------------------------------------------------------------- /src/util.d.ts: -------------------------------------------------------------------------------- 1 | export type OmitProperties = Pick>; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@stellar/tsconfig", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationDir": "dist", 6 | "lib": ["es2015", "dom"], 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "outDir": "dist", 11 | "target": "es5", 12 | "skipLibCheck": true, 13 | "sourceMap": true, 14 | "allowSyntheticDefaultImports": true 15 | }, 16 | "include": ["src"], 17 | "exclude": [ 18 | "node_modules", 19 | "node_modules/axios", 20 | "dist", 21 | "src/browser.ts", 22 | "playground/src/@stellar", 23 | "documentation/src/docs.json", 24 | "src/fixtures" 25 | ], 26 | "typedocOptions": { 27 | "out": "docs", 28 | "mode": "modules", 29 | "exclude": ["**/*.test.ts", "**/node_modules/**"], 30 | "excludePrivate": true, 31 | "includeDeclarations": true, 32 | "json": "dist/docs.json" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@stellar/tslint-config"], 3 | "rules": { 4 | "variable-name": { 5 | "options": [ 6 | "ban-keywords", 7 | "allow-leading-underscore", 8 | "allow-pascal-case", 9 | "allow-snake-case" 10 | ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const TerserPlugin = require("terser-webpack-plugin"); 4 | 5 | module.exports = { 6 | entry: { 7 | "wallet-sdk": "./src/browser.ts", 8 | "wallet-sdk.min": "./src/browser.ts", 9 | }, 10 | output: { 11 | filename: "[name].js", 12 | path: path.resolve(__dirname, "dist/commonjs"), 13 | libraryTarget: "commonjs-module", 14 | }, 15 | resolve: { 16 | extensions: [".js", ".json", ".ts"], 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.ts$/, 22 | exclude: /node_modules/, 23 | use: [ 24 | { 25 | loader: "ts-loader", 26 | options: { 27 | transpileOnly: true, 28 | }, 29 | }, 30 | ], 31 | }, 32 | { 33 | test: /\.js$/, 34 | exclude: /node_modules\/(?!(crc)\/).*/, 35 | use: [ 36 | { 37 | loader: "babel-loader", 38 | }, 39 | ], 40 | }, 41 | ], 42 | }, 43 | plugins: [ 44 | new webpack.optimize.LimitChunkCountPlugin({ 45 | maxChunks: 1, 46 | }), 47 | ], 48 | optimization: { 49 | minimize: true, 50 | minimizer: [ 51 | new TerserPlugin({ 52 | test: /\.min\.js$/, 53 | }), 54 | ], 55 | }, 56 | }; 57 | --------------------------------------------------------------------------------