├── .eslintrc ├── public ├── favicon.ico ├── manifest.json └── index.html ├── src ├── index.css ├── __mocks__ │ └── MockXHR.js ├── components │ ├── NoMatch.js │ ├── Spinner.js │ ├── CreateButton.js │ ├── NetworkSelector.js │ ├── contracts │ │ ├── JointAccountCustom.js │ │ ├── JointAccountEqual.js │ │ ├── MofNSigners.js │ │ ├── JointAccountBase.js │ │ ├── Token.js │ │ └── ROSCARotatedSavings.js │ ├── fields │ │ ├── SignerWithWeightField.js │ │ ├── __tests__ │ │ │ ├── AccountWithHelpersField.test.js │ │ │ └── __snapshots__ │ │ │ │ └── AccountWithHelpersField.test.js.snap │ │ └── AccountWithHelpersField.js │ ├── Receipt.js │ ├── Menu.js │ └── SignIn.js ├── index.js ├── __tests__ │ ├── App.test.js │ └── utils.test.js ├── App.css ├── api │ ├── index.js │ ├── m_of_n_signers.js │ ├── __tests__ │ │ ├── joint_account.test.js │ │ └── __snapshots__ │ │ │ └── joint_account.test.js.snap │ ├── joint_account.js │ ├── rosca_rotated_savings.js │ └── token.js ├── stellar.js ├── logo.svg ├── utils.js ├── registerServiceWorker.js └── App.js ├── static.json ├── .travis.yml ├── .gitignore ├── README.md ├── package.json └── LICENSE /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatch/stellar-contracts/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /static.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "build/", 3 | "routes": { 4 | "/**": "index.html" 5 | }, 6 | "https_only": true 7 | } 8 | -------------------------------------------------------------------------------- /src/__mocks__/MockXHR.js: -------------------------------------------------------------------------------- 1 | const xhrMockClass = () => ({ 2 | open: jest.fn(), 3 | send: jest.fn(), 4 | setRequestHeader: jest.fn(), 5 | }) 6 | 7 | window.XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass) 8 | -------------------------------------------------------------------------------- /src/components/NoMatch.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Grid, Row} from 'react-bootstrap' 3 | 4 | const NoMatch = () => 5 | 6 | 7 |

Oops, nothing here ...

8 |
9 |
10 | 11 | export default NoMatch 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: node_js 3 | node_js: 4 | - lts/* 5 | - node 6 | cache: 7 | directories: 8 | - node_modules 9 | script: 10 | - CI=true npm test 11 | - CI=false npm run build # CI=false so warnings (from stellar-base>bindings) don't fail the build 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | registerServiceWorker(); 9 | -------------------------------------------------------------------------------- /src/__tests__/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from '../App' 4 | 5 | import '../__mocks__/MockXHR.js' 6 | 7 | it('renders without crashing', () => { 8 | const div = document.createElement('div') 9 | ReactDOM.render(, div) 10 | }) 11 | -------------------------------------------------------------------------------- /src/components/Spinner.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import MDSpinner from 'react-md-spinner' 3 | 4 | const Spinner = () => 5 | 12 | 13 | export default Spinner 14 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | body, 2 | .navbar { 3 | font-family: "Noto Sans", "Noto Sans CJK {SC, TC}", sans-serif; 4 | } 5 | 6 | .App-header { 7 | height: 60px; 8 | padding: 10px; 9 | } 10 | 11 | .Network-Selector button:focus { 12 | outline: none; 13 | } 14 | 15 | .Network-Selector button:not(:first-child) { 16 | margin-left: 5px; 17 | } 18 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Stellar Contracts", 3 | "name": "Stellar Contracts", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .env 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | yarn.lock 24 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Stellar Contracts Wallet 10 | 11 | 12 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import sdk from 'stellar-sdk' 2 | 3 | import Token from './token' 4 | import JointAccount from './joint_account' 5 | import MofNSigners from './m_of_n_signers' 6 | import ROSCARotatedSavings from './rosca_rotated_savings' 7 | 8 | class Contracts { 9 | constructor(server) { 10 | this.server = server 11 | } 12 | 13 | token() { 14 | return new Token(sdk, this.server) 15 | } 16 | 17 | jointAccount() { 18 | return new JointAccount(sdk, this.server) 19 | } 20 | 21 | mOfNSigners() { 22 | return new MofNSigners(sdk, this.server) 23 | } 24 | 25 | roscaRotatedSavings() { 26 | return new ROSCARotatedSavings(sdk, this.server) 27 | } 28 | } 29 | 30 | export default Contracts 31 | -------------------------------------------------------------------------------- /src/__tests__/utils.test.js: -------------------------------------------------------------------------------- 1 | import sdk from 'stellar-sdk' 2 | 3 | import {accountExists} from '../utils' 4 | 5 | const VALID_PUBLICKEY = sdk.Keypair.random().publicKey() 6 | 7 | it('accountExists catches and processes NotFoundError', async () => { 8 | const serverRetNotFoundError = { 9 | loadAccount: jest.fn(() => Promise.reject(new sdk.NotFoundError())), 10 | } 11 | expect(await accountExists(VALID_PUBLICKEY, serverRetNotFoundError)).toBe( 12 | false 13 | ) 14 | }) 15 | 16 | it('accountExists returns true when account exists', async () => { 17 | const serverRetAccount = { 18 | loadAccount: jest.fn(acc => Promise.resolve(new sdk.Account(acc, '1'))), 19 | } 20 | expect(await accountExists(VALID_PUBLICKEY, serverRetAccount)).toBe(true) 21 | }) 22 | -------------------------------------------------------------------------------- /src/components/CreateButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import {Button} from 'react-bootstrap' 4 | import Spinner from './Spinner' 5 | 6 | /** 7 | * Create button with: 8 | * - loading spinner 9 | * - error message 10 | */ 11 | const CreateButton = ({errorMsg, isLoading}) => 12 | 13 | 16 | 17 | {isLoading === true && } 18 | {errorMsg && 19 | errorMsg != null && 20 | 21 | Creation failed: {errorMsg} 22 | } 23 | 24 | 25 | 26 | CreateButton.propTypes = { 27 | errorMsg: PropTypes.string, 28 | isLoading: PropTypes.bool.isRequired, 29 | } 30 | 31 | export default CreateButton 32 | -------------------------------------------------------------------------------- /src/components/NetworkSelector.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Button} from 'react-bootstrap' 3 | import {networks} from '../stellar' 4 | 5 | const NetworkButton = ({network, selectedNetwork, switcher}) => 6 | 12 | 13 | const NetworkSelector = props => 14 |
15 | {Object.keys(networks) 16 | .filter(network => networks[network].hide !== true) 17 | .map(network => 18 | 25 | )} 26 |
27 | 28 | export default NetworkSelector 29 | -------------------------------------------------------------------------------- /src/stellar.js: -------------------------------------------------------------------------------- 1 | import sdk from 'stellar-sdk' 2 | 3 | /* ---------------------------------------------------------- 4 | * 5 | * Stellar networks 6 | * 7 | * ---------------------------------------------------------*/ 8 | 9 | const newServer = (address, allowHttp = false) => 10 | new sdk.Server(address, {allowHttp}) 11 | 12 | const usePubnetServer = () => { 13 | sdk.Network.usePublicNetwork() 14 | return newServer(networks.public.address) 15 | } 16 | 17 | const useTestnetServer = () => { 18 | sdk.Network.useTestNetwork() 19 | return newServer(networks.test.address) 20 | } 21 | 22 | const useLocalServer = () => { 23 | return newServer(networks.local.address, true) 24 | } 25 | 26 | const networks = { 27 | public: { 28 | address: 'https://horizon.stellar.org', 29 | initFunc: usePubnetServer, 30 | }, 31 | test: { 32 | address: 'https://horizon-testnet.stellar.org', 33 | initFunc: useTestnetServer, 34 | }, 35 | local: { 36 | address: 'http://localhost:8000', 37 | initFunc: useLocalServer, 38 | hide: true, // from UI 39 | }, 40 | } 41 | 42 | export {sdk, networks} 43 | -------------------------------------------------------------------------------- /src/components/contracts/JointAccountCustom.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Panel} from 'react-bootstrap' 3 | import JointAccountBase from './JointAccountBase' 4 | 5 | const formData = { 6 | jointAccount: {secretKey: ''}, 7 | members: [{publicKey: '', weight: 1}], 8 | } 9 | 10 | const HelpPanel = () => 11 | 12 |
13 | Create a joint account setting weights and thresholds. This setup allows a 14 | range of joint account types to be setup. 15 |
16 |
17 | References: 18 |
19 | 24 | 29 |
30 |
31 |
32 | 33 | export default () => 34 | 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NOTE: ARCHIVED - This old repository presented patterns for working with what Stellar "basic" contracts at the time. However Stellar is now introducing Soroban contracts which are much more advanced smart contracts. Archiving this first because no activity but second to avoid confusion with the new contracts. 2 | 3 | # stellar-contracts 4 | [![Build Status](https://travis-ci.org/chatch/stellar-contracts.svg?branch=master)](https://travis-ci.org/chatch/stellar-contracts) 5 | 6 | https://stellar-contracts.herokuapp.com 7 | 8 | This site presents smart contract patterns for the [Stellar Network](https://stellar.org). See [this post](https://www.stellar.org/blog/multisig-and-simple-contracts-stellar/) for some background on contracts on Stellar. 9 | 10 | Each contract can be deployed to the Stellar Network using the contract forms. You'll receive a JSON receipt with details of transactions, accounts created, etc. 11 | 12 | Contract forms are React components that are reusable in other sites (i will add an example setup for this). This means you could easily drop one of these into your React or Angular (via ngReact) wallet to provide one of the contract setups to your users. 13 | 14 | ## Get started 15 | ``` 16 | npm install 17 | npm start 18 | ``` 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stellar-contracts", 3 | "version": "0.0.1", 4 | "description": "Stellar 'Simple Contracts' catalog and examples", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/chatch/stellar-contracts.git" 8 | }, 9 | "keywords": [ 10 | "react", 11 | "smart contracts", 12 | "stellar" 13 | ], 14 | "author": "Chris Hatch", 15 | "license": "Apache-2.0", 16 | "bugs": { 17 | "url": "https://github.com/chatch/stellar-contracts/issues" 18 | }, 19 | "engines": { 20 | "node": "8.x.x" 21 | }, 22 | "dependencies": { 23 | "react": "^15.6.1", 24 | "react-bootstrap": "^0.31.0", 25 | "react-copy-to-clipboard": "^5.0.0", 26 | "react-dom": "^15.6.1", 27 | "react-json-pretty": "^1.7.9", 28 | "react-jsonschema-form": "^0.49.0", 29 | "react-md-spinner": "^0.2.5", 30 | "react-router-bootstrap": "^0.24.2", 31 | "react-router-dom": "^4.1.1", 32 | "react-scripts": "^1.1.4", 33 | "recompose": "^0.23.5", 34 | "stellar-sdk": "0.9.1" 35 | }, 36 | "devDependencies": { 37 | "bootstrap": "^3.3.7", 38 | "enzyme": "^2.9.1", 39 | "node-localstorage": "^1.3.0", 40 | "react-test-renderer": "^15.6.1" 41 | }, 42 | "scripts": { 43 | "start": "react-scripts start", 44 | "build": "react-scripts build", 45 | "test": "react-scripts test --env=jsdom", 46 | "eject": "react-scripts eject", 47 | "analyze": "source-map-explorer build/static/js/main.*" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/contracts/JointAccountEqual.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Panel} from 'react-bootstrap' 3 | import JointAccountBase from './JointAccountBase' 4 | 5 | const formData = { 6 | jointAccount: {secretKey: ''}, 7 | members: [ 8 | {publicKey: '', weight: 1}, 9 | {publicKey: '', weight: 1}, 10 | {publicKey: '', weight: 1}, 11 | ], 12 | thresholds: {low: 3, med: 3, high: 3, masterWeight: 0}, 13 | } 14 | 15 | const HelpPanel = () => 16 | 17 |
Creates a simple joint account on the Stellar Network.
18 |
19 | This setup allows any member account to make payments (and other medium 20 | threshold operations). 21 |
22 |
23 | However high threshold operations like changing the list of signers 24 | requires all parties to sign. 25 |
26 |
27 | References: 28 |
29 | 34 | 39 |
40 |
41 |
42 | 43 | export default () => 44 | 45 | -------------------------------------------------------------------------------- /src/api/m_of_n_signers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create or setup existing account with M of N signers scheme. 3 | */ 4 | 5 | class MofNSigners { 6 | constructor(sdk, server) { 7 | this.sdk = sdk 8 | this.server = server 9 | } 10 | 11 | async create({ 12 | members, 13 | signingKey, 14 | numSignersLow, 15 | numSignersMed, 16 | numSignersHigh, 17 | }) { 18 | const signingKeypair = this.sdk.Keypair.fromSecret(signingKey) 19 | const signingAccount = await this.server.loadAccount( 20 | signingKeypair.publicKey() 21 | ) 22 | 23 | const Operation = this.sdk.Operation 24 | const txBuilder = new this.sdk.TransactionBuilder(signingAccount) 25 | 26 | // Add signers operations 27 | members.forEach(acc => { 28 | if (acc !== signingKeypair.publicKey()) 29 | txBuilder.addOperation( 30 | Operation.setOptions({signer: {ed25519PublicKey: acc, weight: 1}}) 31 | ) 32 | }) 33 | 34 | // Set Thresholds 35 | const numOrN = numSigners => (numSigners ? numSigners : members.length) 36 | txBuilder.addOperation( 37 | Operation.setOptions({ 38 | masterWeight: 1, 39 | numSignersLow: numOrN(numSignersLow), 40 | numSignersMed: numOrN(numSignersMed), 41 | numSignersHigh: numOrN(numSignersHigh), 42 | }) 43 | ) 44 | 45 | const tx = txBuilder.build() 46 | tx.sign(signingKeypair) 47 | 48 | return this.server.submitTransaction(tx).then(res => res).catch(err => { 49 | console.error(JSON.stringify(err)) 50 | throw new Error(err) 51 | }) 52 | } 53 | } 54 | 55 | export default MofNSigners 56 | -------------------------------------------------------------------------------- /src/components/fields/SignerWithWeightField.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Col, FormControl, Row} from 'react-bootstrap' 3 | import PropTypes from 'prop-types' 4 | 5 | /** 6 | * Stellar signer field (for jsonschema forms) that renders these inputs side by side: 7 | * - Account / Public Key text 8 | * - Signer weight number 9 | */ 10 | class SignerWithWeightField extends React.Component { 11 | constructor(props) { 12 | super(props) 13 | console.log( 14 | `SignerWithWeightField: constructor: uiSchema: ${JSON.stringify( 15 | props.uiSchema 16 | )}` 17 | ) 18 | this.state = {...props.formData} 19 | } 20 | 21 | handleOnChange = e => { 22 | const name = e.target.type === 'number' ? 'weight' : 'publicKey' 23 | this.setState({[name]: e.target.value}, () => 24 | this.props.onChange(this.state) 25 | ) 26 | } 27 | 28 | render() { 29 | return ( 30 | 31 | 32 | 38 | 39 | 40 | 45 | 46 | 47 | ) 48 | } 49 | } 50 | 51 | SignerWithWeightField.propTypes = { 52 | formData: PropTypes.object, 53 | onChange: PropTypes.func.isRequired, 54 | uiSchema: PropTypes.object, 55 | } 56 | 57 | export default SignerWithWeightField 58 | -------------------------------------------------------------------------------- /src/components/Receipt.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import {Button, Modal} from 'react-bootstrap' 4 | import CopyToClipboard from 'react-copy-to-clipboard' 5 | import JSONPretty from 'react-json-pretty' 6 | 7 | const ClipboardCopyButton = ({copied, handleOnClickCopy, receipt}) => ( 8 | 9 | 10 | 11 | {copied && Copied!} 12 | 13 | 14 | ) 15 | 16 | const ReceiptModal = ({ 17 | copied, 18 | handleOnClickClose, 19 | handleOnClickCopy, 20 | receipt, 21 | showModal, 22 | }) => ( 23 | 24 | 25 | Contract Receipt 26 | 27 | 28 |
29 | 34 |
35 | 36 |
37 | 38 | 39 | 40 |
41 | ) 42 | 43 | class Receipt extends React.Component { 44 | state = {copied: false, showModal: true} 45 | 46 | handleOnClickClose = () => { 47 | this.setState({showModal: false}) 48 | } 49 | 50 | handleOnClickCopy = () => { 51 | this.setState({copied: true}) 52 | } 53 | 54 | render() { 55 | return ( 56 | 63 | ) 64 | } 65 | } 66 | 67 | Receipt.propTypes = { 68 | receipt: PropTypes.object.isRequired, 69 | } 70 | 71 | export default Receipt 72 | -------------------------------------------------------------------------------- /src/components/fields/__tests__/AccountWithHelpersField.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {shallow} from 'enzyme' 3 | import {Keypair, StrKey} from 'stellar-sdk' 4 | 5 | import AccountWithHelpersField from '../AccountWithHelpersField' 6 | 7 | it('renders with minimal fields', () => { 8 | const onChange = jest.fn() 9 | const field = shallow( 10 | 16 | ) 17 | expect(field.getNodes()).toMatchSnapshot() 18 | }) 19 | 20 | it('renders placeholder', () => { 21 | const onChange = jest.fn() 22 | const placeholder = 'Enter a Stellar Secret Key' 23 | const field = shallow( 24 | 30 | ) 31 | expect(field.find('[type="text"]').prop('placeholder')).toEqual(placeholder) 32 | expect(field.getNodes()).toMatchSnapshot() 33 | }) 34 | 35 | it('generate button inserts a new account', () => { 36 | const onChange = jest.fn() 37 | const field = shallow( 38 | 44 | ) 45 | field.find('#btn-generate').simulate('click') 46 | 47 | const secretKey = field.find('[type="text"]').prop('value') 48 | expect(StrKey.isValidEd25519SecretSeed(secretKey)).toEqual(true) 49 | expect(onChange).toHaveBeenCalledWith({secretKey: secretKey}) 50 | }) 51 | 52 | it('use signer button inserts the signer', () => { 53 | const keypair = Keypair.random() 54 | const formCtx = {signer: keypair.secret()} 55 | const onChange = jest.fn() 56 | const field = shallow( 57 | 63 | ) 64 | field.find('#btn-use-signer').simulate('click') 65 | 66 | const secretKey = field.find('[type="text"]').prop('value') 67 | expect(secretKey).toEqual(keypair.secret()) 68 | expect(onChange).toHaveBeenCalledWith({secretKey: secretKey}) 69 | }) 70 | -------------------------------------------------------------------------------- /src/api/__tests__/joint_account.test.js: -------------------------------------------------------------------------------- 1 | import sdk from 'stellar-sdk' 2 | import JointAccount from '../joint_account' 3 | import {memberList, signerObj, thresholdsObj} from '../../utils' 4 | 5 | const SIGNER = sdk.Keypair.fromSecret( 6 | 'SAP4WQ7W3JS72NGGTJ3X7FM3VGMVI5AIWS5VUOXK4MJECLTOPDUE7VMK' 7 | ) 8 | const JOINT_NEW = sdk.Keypair.fromSecret( 9 | 'SABRXC3RQJVVUS22N7H6WGZPTWJ7GBKIZ3PBGX4NWKZZSLC4DWIUP4K3' 10 | ) 11 | const MEMBER1 = sdk.Keypair.fromPublicKey( 12 | 'GAJ3VIHUHOHPI7HIJ3DS5HJ5K7CMJ5QVVCO4IXBIG32HGM3WAS3OEOMA' 13 | ) 14 | const MEMBER2 = sdk.Keypair.fromPublicKey( 15 | 'GBN4BQAIFUEKYOEYAI4ZEWUK7UW4QVCFVT4VD643L5O7IBCZEES3R6D3' 16 | ) 17 | const MEMBER3 = sdk.Keypair.fromPublicKey( 18 | 'GAGXSBXBSJUYCYM623OQQK5P3PXT2BOCKRXU7TVAEOZB5CET2ABJX2T6' 19 | ) 20 | 21 | const MEMBERS_EQUAL_WEIGHTS = memberList([ 22 | MEMBER1.publicKey(), 23 | MEMBER2.publicKey(), 24 | MEMBER3.publicKey(), 25 | ]) 26 | const MEMBERS_WITH_WEIGHTS = memberList( 27 | [MEMBER1.publicKey(), MEMBER2.publicKey(), MEMBER3.publicKey()], 28 | [2, 1, 1] 29 | ) 30 | 31 | const mockServer = { 32 | loadAccount: jest.fn(account => { 33 | if (account === SIGNER.publicKey()) 34 | return Promise.resolve(new sdk.Account(account, '1')) 35 | else if (account === JOINT_NEW.publicKey()) { 36 | const acc = new sdk.Account(account, '1') 37 | acc.signers = [ 38 | signerObj(MEMBER1.publicKey(), 1), 39 | signerObj(MEMBER2.publicKey(), 1), 40 | signerObj(MEMBER3.publicKey(), 1), 41 | ] 42 | acc.thresholds = thresholdsObj(0, 0, MEMBERS_EQUAL_WEIGHTS) 43 | return Promise.resolve(acc) 44 | } 45 | }), 46 | submitTransaction: jest.fn(() => Promise.resolve({fake: 'receipt'})), 47 | } 48 | 49 | // not using any network but the sdk needs this to be set to something 50 | sdk.Network.useTestNetwork() 51 | 52 | const jointAccount = new JointAccount(sdk, mockServer) 53 | 54 | it('joint account simple equal weights creates ok', async () => { 55 | const inputs = { 56 | accountSecret: JOINT_NEW.secret(), 57 | members: MEMBERS_EQUAL_WEIGHTS, 58 | signerSecret: SIGNER.secret(), 59 | thresholds: Object.assign( 60 | {masterWeight: 1}, 61 | thresholdsObj(0, 0, MEMBERS_EQUAL_WEIGHTS.length) 62 | ), 63 | } 64 | const receipt = await jointAccount.create(inputs) 65 | expect(receipt).toMatchSnapshot() 66 | }) 67 | -------------------------------------------------------------------------------- /src/components/fields/AccountWithHelpersField.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Button, Col, FormControl, Row} from 'react-bootstrap' 3 | import PropTypes from 'prop-types' 4 | import sdk from 'stellar-sdk' 5 | 6 | /** 7 | * Stellar account field (for jsonschema forms) that adds: 8 | * - Generate button - to create a new account 9 | * - Use Signer button - to use account signed into the app 10 | */ 11 | class AccountWithHelpersField extends React.Component { 12 | constructor(props) { 13 | super(props) 14 | this.state = {...props.formData} 15 | } 16 | 17 | handleOnClickGenerate = () => { 18 | const newKeypair = sdk.Keypair.random() 19 | this.setState( 20 | { 21 | secretKey: newKeypair.secret(), 22 | }, 23 | () => this.props.onChange(this.state) 24 | ) 25 | } 26 | 27 | handleOnClickUseSigner = signer => { 28 | this.setState({secretKey: signer}, () => this.props.onChange(this.state)) 29 | } 30 | 31 | handleOnChange = e => { 32 | console.log(`AccountWithHelpersField: onChange: ${e.target.value}`) 33 | this.setState({secretKey: e.target.value}, () => 34 | this.props.onChange(this.state) 35 | ) 36 | } 37 | 38 | render() { 39 | return ( 40 | 41 | 42 | 48 | 49 | 50 | 57 | 58 | 59 | 67 | 68 | 69 | ) 70 | } 71 | } 72 | 73 | AccountWithHelpersField.propTypes = { 74 | formContext: PropTypes.shape({ 75 | signer: PropTypes.string, 76 | }).isRequired, 77 | formData: PropTypes.object, 78 | onChange: PropTypes.func.isRequired, 79 | } 80 | 81 | export default AccountWithHelpersField 82 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/api/__tests__/__snapshots__/joint_account.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`joint account simple equal weights creates ok 1`] = ` 4 | Object { 5 | "inputs": Object { 6 | "accountSecret": "SABRXC3RQJVVUS22N7H6WGZPTWJ7GBKIZ3PBGX4NWKZZSLC4DWIUP4K3", 7 | "members": Array [ 8 | Object { 9 | "publicKey": "GAJ3VIHUHOHPI7HIJ3DS5HJ5K7CMJ5QVVCO4IXBIG32HGM3WAS3OEOMA", 10 | "weight": 1, 11 | }, 12 | Object { 13 | "publicKey": "GBN4BQAIFUEKYOEYAI4ZEWUK7UW4QVCFVT4VD643L5O7IBCZEES3R6D3", 14 | "weight": 1, 15 | }, 16 | Object { 17 | "publicKey": "GAGXSBXBSJUYCYM623OQQK5P3PXT2BOCKRXU7TVAEOZB5CET2ABJX2T6", 18 | "weight": 1, 19 | }, 20 | ], 21 | "signerSecret": "SAP4WQ7W3JS72NGGTJ3X7FM3VGMVI5AIWS5VUOXK4MJECLTOPDUE7VMK", 22 | "thresholds": Object { 23 | "high_threshold": 3, 24 | "low_threshold": 0, 25 | "masterWeight": 1, 26 | "med_threshold": 0, 27 | }, 28 | }, 29 | "jointAccount": Object { 30 | "keys": Object { 31 | "publicKey": "GCKMUQMJXGSPMQRQAZZ2FTZBFCXIXWGWAKGCM3CUVTIR2OOZXB664RSO", 32 | "secret": "SABRXC3RQJVVUS22N7H6WGZPTWJ7GBKIZ3PBGX4NWKZZSLC4DWIUP4K3", 33 | }, 34 | "signers": Array [ 35 | Object { 36 | "key": "GAJ3VIHUHOHPI7HIJ3DS5HJ5K7CMJ5QVVCO4IXBIG32HGM3WAS3OEOMA", 37 | "public_key": "GAJ3VIHUHOHPI7HIJ3DS5HJ5K7CMJ5QVVCO4IXBIG32HGM3WAS3OEOMA", 38 | "type": "ed25519_public_key", 39 | "weight": 1, 40 | }, 41 | Object { 42 | "key": "GBN4BQAIFUEKYOEYAI4ZEWUK7UW4QVCFVT4VD643L5O7IBCZEES3R6D3", 43 | "public_key": "GBN4BQAIFUEKYOEYAI4ZEWUK7UW4QVCFVT4VD643L5O7IBCZEES3R6D3", 44 | "type": "ed25519_public_key", 45 | "weight": 1, 46 | }, 47 | Object { 48 | "key": "GAGXSBXBSJUYCYM623OQQK5P3PXT2BOCKRXU7TVAEOZB5CET2ABJX2T6", 49 | "public_key": "GAGXSBXBSJUYCYM623OQQK5P3PXT2BOCKRXU7TVAEOZB5CET2ABJX2T6", 50 | "type": "ed25519_public_key", 51 | "weight": 1, 52 | }, 53 | ], 54 | "thresholds": Object { 55 | "high_threshold": Array [ 56 | Object { 57 | "publicKey": "GAJ3VIHUHOHPI7HIJ3DS5HJ5K7CMJ5QVVCO4IXBIG32HGM3WAS3OEOMA", 58 | "weight": 1, 59 | }, 60 | Object { 61 | "publicKey": "GBN4BQAIFUEKYOEYAI4ZEWUK7UW4QVCFVT4VD643L5O7IBCZEES3R6D3", 62 | "weight": 1, 63 | }, 64 | Object { 65 | "publicKey": "GAGXSBXBSJUYCYM623OQQK5P3PXT2BOCKRXU7TVAEOZB5CET2ABJX2T6", 66 | "weight": 1, 67 | }, 68 | ], 69 | "low_threshold": 0, 70 | "med_threshold": 0, 71 | }, 72 | }, 73 | "transactions": Object { 74 | "create": Object { 75 | "fake": "receipt", 76 | }, 77 | }, 78 | } 79 | `; 80 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import {getContext} from 'recompose' 3 | import sdk from 'stellar-sdk' 4 | 5 | const keypairReadable = keypair => { 6 | return {publicKey: keypair.publicKey(), secret: keypair.secret()} 7 | } 8 | 9 | const isSignedIn = ({signer}) => 10 | signer && sdk.StrKey.isValidEd25519SecretSeed(signer) 11 | 12 | const accountExists = async (publicKey, server) => { 13 | let exists 14 | try { 15 | await server.loadAccount(publicKey) 16 | exists = true 17 | } catch (e) { 18 | if (e instanceof sdk.NotFoundError) { 19 | exists = false 20 | } else { 21 | console.error(e) 22 | throw new Error(e.msg) 23 | } 24 | } 25 | return exists 26 | } 27 | 28 | const storageInit = () => { 29 | let storage 30 | if (typeof localStorage === 'undefined' || localStorage === null) { 31 | const tmpdir = require('os').tmpdir 32 | const join = require('path').join 33 | const storagePath = join(tmpdir(), 'steexp') 34 | const LocalStorage = require('node-localstorage').LocalStorage 35 | storage = new LocalStorage(storagePath) 36 | } else { 37 | storage = localStorage 38 | } 39 | return storage 40 | } 41 | 42 | const stampMemo = () => sdk.Memo.text('Created using: git.io/v7b6s') 43 | 44 | const memberList = (accArr, weightsArr) => 45 | accArr.map((acc, idx) => { 46 | const haveWeights = weightsArr && weightsArr.length === accArr.length 47 | return {publicKey: acc, weight: haveWeights ? weightsArr[idx] : 1} 48 | }) 49 | 50 | /** 51 | * See here for how to calculate the minimum account balance: 52 | * https://www.stellar.org/developers/guides/concepts/fees.html#minimum-account-balance 53 | * 54 | * Entries include Trustlines, Offers, Signers, Data entries 55 | * 56 | * See fetchBaseReserve() for getting the latest reserve amount. 57 | */ 58 | 59 | const minAccountBalance = (numEntries, baseReserve) => 60 | (2 + numEntries) * baseReserve 61 | 62 | const fetchBaseReserve = server => 63 | server 64 | .ledgers() 65 | .limit(1) 66 | .order('desc') 67 | .call() 68 | .then(l => l.records[0].base_reserve) 69 | 70 | const signerObj = (key, weight) => { 71 | return {weight: weight, type: 'ed25519_public_key', key: key, public_key: key} 72 | } 73 | 74 | const thresholdsObj = (low, med, high) => { 75 | return { 76 | low_threshold: low, 77 | med_threshold: med, 78 | high_threshold: high, 79 | } 80 | } 81 | 82 | // @see App.js which puts this stellar server handle on the context 83 | const withServer = getContext({server: PropTypes.object}) 84 | const withSigner = getContext({signer: PropTypes.string}) 85 | 86 | export { 87 | accountExists, 88 | fetchBaseReserve, 89 | keypairReadable, 90 | isSignedIn, 91 | memberList, 92 | minAccountBalance, 93 | signerObj, 94 | stampMemo, 95 | storageInit, 96 | thresholdsObj, 97 | withServer, 98 | withSigner, 99 | } 100 | -------------------------------------------------------------------------------- /src/components/Menu.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Col, Grid, Panel, Row} from 'react-bootstrap' 3 | import {Link} from 'react-router-dom' 4 | 5 | const contracts = [ 6 | { 7 | path: '/token', 8 | name: 'Token', 9 | description: 'Issue a New Token on Stellar', 10 | }, 11 | { 12 | path: '/joint-account/custom', 13 | name: 'Joint Account (Customize)', 14 | description: 'Create a joint account setting custom weights and thresholds', 15 | }, 16 | { 17 | path: '/joint-account/equal', 18 | name: 'Joint Account (Preset: Equal Weights)', 19 | description: 'Simple joint account with equal weights for all members', 20 | }, 21 | { 22 | path: '/rosca/rotated-savings', 23 | name: 'ROSCA (rotated savings)', 24 | description: 'Setup a "Rotated Savings" / "Merry-Go-Round" style ROSCA', 25 | }, 26 | { 27 | path: '/m-of-n-todo', 28 | name: 'M of N Signers [TODO]', 29 | description: 'Create accounts with M of N Signer schemes', 30 | }, 31 | { 32 | path: '/bond', 33 | name: 'Bonds [TODO]', 34 | description: 'Issue Bonds', 35 | }, 36 | { 37 | path: '/channel', 38 | name: 'Channels [TODO]', 39 | description: 'Setup Payment Channels', 40 | }, 41 | ] 42 | 43 | const IntroductionPanel = () => 44 | 45 |
46 | This is a wallet that presents smart contract patterns for the{' '} 47 | Stellar Network. See{' '} 48 | 49 | this post 50 | {' '} 51 | for some background on contracts on Stellar. 52 |
53 |
54 | To setup a contract, fill in the contract form and click Create to submit 55 | it to the network. You will receive a JSON receipt with full details of 56 | inputs, accounts created, transaction hashes, etc. 57 |
58 |
59 | WARNING: This is BETA software. Test these contracts thoroughly on testnet 60 | before using them on the public network. 61 |
62 |
63 | 64 | class Menu extends React.Component { 65 | render() { 66 | return ( 67 | 68 | 69 | 70 | 71 |

Contracts

72 |
73 | {contracts.map(c => 74 | 75 | 76 | 77 | {c.name} 78 | 79 | 80 | 81 | {c.description} 82 | 83 | 84 | )} 85 | 86 | 87 | 88 | 89 |
90 |
91 | ) 92 | } 93 | } 94 | 95 | export default Menu 96 | -------------------------------------------------------------------------------- /src/components/fields/__tests__/__snapshots__/AccountWithHelpersField.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders placeholder 1`] = ` 4 | Array [ 5 | 9 | 14 | 22 | 23 | 28 | 39 | 40 | 45 | 56 | 57 | , 58 | ] 59 | `; 60 | 61 | exports[`renders with minimal fields 1`] = ` 62 | Array [ 63 | 67 | 72 | 80 | 81 | 86 | 97 | 98 | 103 | 114 | 115 | , 116 | ] 117 | `; 118 | -------------------------------------------------------------------------------- /src/components/contracts/MofNSigners.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Form from 'react-jsonschema-form' 3 | import {Button} from 'react-bootstrap' 4 | import sdk from 'stellar-sdk' 5 | 6 | import Contracts from '../../api' 7 | import {withServer, withSigner} from '../../utils' 8 | 9 | const schema = { 10 | title: 'M of N Signers', 11 | description: 12 | 'Setup M of N Signers on an account. Leave account blank to create a new one.', 13 | type: 'object', 14 | properties: { 15 | members: { 16 | title: 'Member Accounts', 17 | type: 'array', 18 | items: { 19 | type: 'string', 20 | default: '', 21 | }, 22 | }, 23 | numSignersLow: { 24 | title: 'Num signers required to pass low threshold', 25 | type: 'number', 26 | }, 27 | numSignersMed: { 28 | title: 'Num signers required to pass medium threshold', 29 | type: 'number', 30 | }, 31 | numSignersHigh: { 32 | title: 'Num signers required to pass high threshold', 33 | type: 'number', 34 | }, 35 | signingKey: { 36 | title: 37 | 'Signing key of the joint account creator (the 1st account in the members list above)', 38 | type: 'string', 39 | }, 40 | }, 41 | } 42 | 43 | const uiSchema = { 44 | members: { 45 | 'ui:options': { 46 | orderable: false, 47 | }, 48 | }, 49 | } 50 | 51 | const formData = { 52 | members: ['', ''], 53 | signingKey: '', 54 | } 55 | 56 | class MofNSigners extends React.Component { 57 | state = {} 58 | 59 | formValidate(formData, errors) { 60 | formData.members.forEach((member, idx) => { 61 | if (!sdk.StrKey.isValidEd25519PublicKey(member)) { 62 | errors.members[idx].addError( 63 | 'Account is not a valid ed25519 public key' 64 | ) 65 | } 66 | }) 67 | 68 | if (formData.members.length <= 1) { 69 | errors.members.addError( 70 | 'Need at least 2 member accounts to form an M of N signers account' 71 | ) 72 | } 73 | 74 | if (!sdk.StrKey.isValidEd25519SecretSeed(formData.signingKey)) { 75 | errors.signingKey.addError( 76 | 'Signing key is not a valid ed25519 secret seed' 77 | ) 78 | } 79 | 80 | return errors 81 | } 82 | 83 | handleOnSubmit = ({formData}) => { 84 | console.log(`FORM DATA: ${JSON.stringify(formData)}`) 85 | const contracts = new Contracts(this.props.server) 86 | const contract = contracts.mOfNSigners() 87 | contract.create(formData).then(createRsp => { 88 | console.log(JSON.stringify(createRsp)) 89 | this.setState({receipt: createRsp}) 90 | }) 91 | } 92 | 93 | render() { 94 | return ( 95 |
96 |
103 | 106 |
107 | {this.state.receipt && 108 |
109 | {this.state.receipt} 110 |
} 111 |
112 | ) 113 | } 114 | } 115 | 116 | export default withServer(withSigner(MofNSigners)) 117 | -------------------------------------------------------------------------------- /src/api/joint_account.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a joint account where any member can make a payment. 3 | * ALL members are required to make a change to the member list. 4 | */ 5 | 6 | import {accountExists, keypairReadable, stampMemo} from '../utils' 7 | 8 | // TODO pull this from the latest ledger 9 | const BASE_RESERVE = 10 10 | 11 | class JointAccount { 12 | constructor(sdk, server) { 13 | this.sdk = sdk 14 | this.server = server 15 | } 16 | 17 | async create(props) { 18 | //console.log(`create() props: ${JSON.stringify(props)}`) 19 | const {accountSecret, members, signerSecret, thresholds} = props 20 | 21 | const accountKeypair = this.sdk.Keypair.fromSecret(accountSecret) 22 | 23 | const tx = await this.buildTransaction( 24 | accountKeypair, 25 | members, 26 | signerSecret, 27 | thresholds 28 | ) 29 | 30 | const txResponse = await this.server 31 | .submitTransaction(tx) 32 | .then(res => res) 33 | .catch(err => { 34 | console.error(`Failed in create joint account tx:`) 35 | console.error(err) 36 | return Promise.rejected(err) 37 | }) 38 | 39 | const jointAccountDetails = await this.server.loadAccount( 40 | accountKeypair.publicKey() 41 | ) 42 | 43 | return Promise.resolve({ 44 | jointAccount: { 45 | keys: keypairReadable(accountKeypair), 46 | signers: jointAccountDetails.signers, 47 | thresholds: jointAccountDetails.thresholds, 48 | }, 49 | transactions: {create: txResponse}, 50 | inputs: props, 51 | }) 52 | } 53 | 54 | async buildTransaction(accountKeypair, members, signerSecret, thresholds) { 55 | const Operation = this.sdk.Operation 56 | const signerKeypair = this.sdk.Keypair.fromSecret(signerSecret) 57 | const signerAccount = await this.server.loadAccount( 58 | signerKeypair.publicKey() 59 | ) 60 | const txBuilder = new this.sdk.TransactionBuilder(signerAccount) 61 | 62 | // check if the joint account exists - if not a create_account op is needed to generate it 63 | if (!(await accountExists(accountKeypair.publicKey(), this.server))) { 64 | // see https://www.stellar.org/developers/guides/concepts/fees.html#minimum-account-balance 65 | const startBal = BASE_RESERVE * (members.length + 2) 66 | txBuilder.addOperation( 67 | Operation.createAccount({ 68 | destination: accountKeypair.publicKey(), 69 | startingBalance: String(startBal), 70 | }) 71 | ) 72 | } 73 | 74 | // Add signerSecret operations 75 | members.forEach(({publicKey, weight}) => { 76 | if (publicKey !== signerAccount.accountId()) { 77 | txBuilder.addOperation( 78 | Operation.setOptions({ 79 | signer: {ed25519PublicKey: publicKey, weight: weight}, 80 | source: accountKeypair.publicKey(), 81 | }) 82 | ) 83 | } 84 | }) 85 | 86 | // Set Thresholds 87 | txBuilder.addOperation( 88 | Operation.setOptions( 89 | Object.assign({source: accountKeypair.publicKey()}, thresholds) 90 | ) 91 | ) 92 | 93 | // Add stamp 94 | txBuilder.addMemo(stampMemo()) 95 | 96 | // Sign and return 97 | const tx = txBuilder.build() 98 | tx.sign(signerKeypair, accountKeypair) 99 | return tx 100 | } 101 | } 102 | 103 | export default JointAccount 104 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (!isLocalhost) { 36 | // Is not local host. Just register service worker 37 | registerValidSW(swUrl); 38 | } else { 39 | // This is running on localhost. Lets check if a service worker still exists or not. 40 | checkValidServiceWorker(swUrl); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function registerValidSW(swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then(registration => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing; 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === 'installed') { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log('New content is available; please refresh.'); 60 | } else { 61 | // At this point, everything has been precached. 62 | // It's the perfect time to display a 63 | // "Content is cached for offline use." message. 64 | console.log('Content is cached for offline use.'); 65 | } 66 | } 67 | }; 68 | }; 69 | }) 70 | .catch(error => { 71 | console.error('Error during service worker registration:', error); 72 | }); 73 | } 74 | 75 | function checkValidServiceWorker(swUrl) { 76 | // Check if the service worker can be found. If it can't reload the page. 77 | fetch(swUrl) 78 | .then(response => { 79 | // Ensure service worker exists, and that we really are getting a JS file. 80 | if ( 81 | response.status === 404 || 82 | response.headers.get('content-type').indexOf('javascript') === -1 83 | ) { 84 | // No service worker found. Probably a different app. Reload the page. 85 | navigator.serviceWorker.ready.then(registration => { 86 | registration.unregister().then(() => { 87 | window.location.reload(); 88 | }); 89 | }); 90 | } else { 91 | // Service worker found. Proceed as normal. 92 | registerValidSW(swUrl); 93 | } 94 | }) 95 | .catch(() => { 96 | console.log( 97 | 'No internet connection found. App is running in offline mode.' 98 | ); 99 | }); 100 | } 101 | 102 | export function unregister() { 103 | if ('serviceWorker' in navigator) { 104 | navigator.serviceWorker.ready.then(registration => { 105 | registration.unregister(); 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/api/rosca_rotated_savings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Setup a ROSCA Rotated Savings club given a list of members and ROSCA 3 | * parameters. 4 | */ 5 | 6 | import { 7 | accountExists, 8 | fetchBaseReserve, 9 | minAccountBalance, 10 | keypairReadable, 11 | stampMemo, 12 | } from '../utils' 13 | 14 | class ROSCARotatedSavings { 15 | constructor(sdk, server) { 16 | this.sdk = sdk 17 | this.server = server 18 | } 19 | 20 | create(props) { 21 | console.log(`create() props: ${JSON.stringify(props)}`) 22 | const roscaKeypair = this.sdk.Keypair.random() 23 | return this.validateInput(props) 24 | .then(() => this.buildTransaction({...props, roscaKeypair: roscaKeypair})) 25 | .then(tx => this.server.submitTransaction(tx)) 26 | .then(txResponse => { 27 | return { 28 | roscaAccount: keypairReadable(roscaKeypair), 29 | transactions: {create: txResponse}, 30 | inputs: props, 31 | } 32 | }) 33 | } 34 | 35 | async validateInput({ 36 | assetCode, 37 | assetIssuer, 38 | depositAmount, 39 | members, 40 | signerSecret, 41 | startDate, 42 | }) { 43 | const errors = [] 44 | if ((await accountExists(assetIssuer, this.server)) === false) { 45 | errors.push(`Provided asset issuer does not exist on the network`) 46 | } else { 47 | // check all members have trustline to the asset issuer with minimum limit 48 | const memberAccounts = await Promise.all( 49 | members.map(member => this.server.loadAccount(member)) 50 | ) 51 | memberAccounts.forEach(account => { 52 | const assetCodeMatch = account.balances.filter( 53 | bal => 54 | bal['asset_code'] === assetCode && 55 | bal['asset_issuer'] === assetIssuer 56 | ) 57 | if (assetCodeMatch.length === 0) { 58 | errors.push( 59 | `Member ${account.id} does not have a trustline to the asset code/issuer pair` 60 | ) 61 | } else if (assetCodeMatch.limit < depositAmount) { 62 | errors.push( 63 | `Member ${account.id} has trustline but limit is below the required daily deposit amount` 64 | ) 65 | } 66 | }) 67 | } 68 | 69 | return errors.length > 0 70 | ? Promise.reject(new Error(JSON.stringify(errors))) 71 | : Promise.resolve({}) 72 | } 73 | 74 | async buildTransaction({ 75 | assetCode, 76 | assetIssuer, 77 | depositAmount, 78 | members, 79 | roscaKeypair, 80 | signerSecret, 81 | startDate, 82 | }) { 83 | const signerKeypair = this.sdk.Keypair.fromSecret(signerSecret) 84 | const signerAccount = await this.server.loadAccount( 85 | signerKeypair.publicKey() 86 | ) 87 | 88 | const txBuilder = new this.sdk.TransactionBuilder(signerAccount) 89 | const Operation = this.sdk.Operation 90 | 91 | // 92 | // Create ROSCA account - main collector and payout account 93 | // 94 | 95 | const startBalance = minAccountBalance( 96 | members.length, 97 | await fetchBaseReserve(this.server) 98 | ) 99 | txBuilder.addOperation( 100 | Operation.createAccount({ 101 | destination: roscaKeypair.publicKey(), 102 | startingBalance: String(startBalance), 103 | }) 104 | ) 105 | 106 | // 107 | // Add members as signers 108 | // 109 | 110 | members.forEach(account => { 111 | txBuilder.addOperation( 112 | Operation.setOptions({ 113 | signer: {ed25519PublicKey: account, weight: 1}, 114 | source: roscaKeypair.publicKey(), 115 | }) 116 | ) 117 | }) 118 | 119 | // 120 | // Trust the issuer deposits payments can be received 121 | // 122 | 123 | const asset = new this.sdk.Asset(assetCode, assetIssuer) 124 | const limit = members.length * depositAmount // trust for up to the daily payout amount 125 | txBuilder.addOperation( 126 | Operation.changeTrust({ 127 | asset: asset, 128 | limit: String(limit), 129 | }) 130 | ) 131 | 132 | // Add data key value for new contract 133 | // AND add this to all contracts ...... 134 | // AND have the wallet look it up on startup or signer signin 135 | 136 | // Add stamp 137 | txBuilder.addMemo(stampMemo()) 138 | 139 | // Sign and return 140 | const tx = txBuilder.build() 141 | tx.sign(signerKeypair) 142 | tx.sign(roscaKeypair) 143 | return tx 144 | } 145 | } 146 | 147 | export default ROSCARotatedSavings 148 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import PropTypes from 'prop-types' 3 | import {LinkContainer} from 'react-router-bootstrap' 4 | import {BrowserRouter as Router, Link, Route, Switch} from 'react-router-dom' 5 | import {Col, Grid, Nav, Navbar, NavItem, Row} from 'react-bootstrap' 6 | 7 | import Menu from './components/Menu' 8 | import NetworkSelector from './components/NetworkSelector' 9 | import NoMatch from './components/NoMatch' 10 | import SignIn from './components/SignIn' 11 | 12 | import JointAccountCustom from './components/contracts/JointAccountCustom' 13 | import JointAccountEqual from './components/contracts/JointAccountEqual' 14 | import MofNSigners from './components/contracts/MofNSigners' 15 | import ROSCARotatedSavings from './components/contracts/ROSCARotatedSavings' 16 | import Token from './components/contracts/Token' 17 | 18 | import {networks} from './stellar' 19 | import {storageInit} from './utils' 20 | 21 | import 'bootstrap/dist/css/bootstrap.min.css' 22 | import 'react-json-pretty/src/JSONPretty.1337.css' 23 | import './App.css' 24 | 25 | const storage = storageInit() 26 | const initialNetwork = storage.getItem('network') || 'test' 27 | const initialSigner = storage.getItem('signer') || null 28 | const reloadPage = () => window.location.reload(true) 29 | 30 | class App extends Component { 31 | state = { 32 | network: initialNetwork, 33 | server: networks[initialNetwork].initFunc(), 34 | signer: initialSigner, 35 | } 36 | 37 | networkSwitcher = selectedNetwork => { 38 | console.log(`NETWORK change: ${this.state.network} to ${selectedNetwork}`) 39 | storage.setItem('network', selectedNetwork) 40 | const server = networks[selectedNetwork].initFunc() 41 | this.setState( 42 | { 43 | network: selectedNetwork, 44 | server: server, 45 | }, 46 | reloadPage 47 | ) 48 | } 49 | 50 | onSetSigner = key => { 51 | this.setState({signer: key}) 52 | storage.setItem('signer', key) 53 | } 54 | 55 | onUnSetSigner = () => { 56 | this.setState({signer: null}) 57 | storage.removeItem('signer') 58 | } 59 | 60 | // @see HOCs.js withServer() to get this as props in any component 61 | getChildContext() { 62 | return {server: this.state.server, signer: this.state.signer} 63 | } 64 | 65 | render() { 66 | return ( 67 | 68 |
69 |
70 | 71 | 72 | 73 | 74 | Stellar Contracts Wallet 75 | 76 | 77 | 78 | 79 | 80 | 85 | 86 | 90 | 91 | 92 | 96 | 97 | 98 | 99 |
100 | 101 | 102 | 103 | 104 | 109 | 114 | 115 | 116 | 121 | 122 | 123 | 124 | 132 | 133 | Github 134 | 135 | 136 | 137 |
138 |
139 | ) 140 | } 141 | } 142 | 143 | App.childContextTypes = { 144 | server: PropTypes.object, 145 | signer: PropTypes.string, 146 | } 147 | 148 | export default App 149 | -------------------------------------------------------------------------------- /src/components/SignIn.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import {Button, Modal} from 'react-bootstrap' 4 | import Form from 'react-jsonschema-form' 5 | import truncate from 'lodash/truncate' 6 | import {Keypair, StrKey} from 'stellar-sdk' 7 | import {withServer, withSigner} from '../utils' 8 | 9 | const schema = { 10 | description: 'Enter signing key of the contract signing account:', 11 | type: 'object', 12 | properties: { 13 | signingKey: { 14 | title: 'Signing Key', 15 | type: 'string', 16 | }, 17 | }, 18 | } 19 | 20 | const formData = { 21 | signingKey: '', 22 | } 23 | 24 | const uiSchema = { 25 | signingKey: { 26 | 'ui:autofocus': true, 27 | }, 28 | } 29 | 30 | const SignInButton = ({handleOpen}) => 31 | 34 | 35 | // change account address to window.location.hostname or something context aware 36 | // testit out... 37 | const SignedInWithSignOutButton = ({ 38 | accountUrl, 39 | balance, 40 | handleOnSignOut, 41 | publicKey, 42 | }) => 43 | 44 | 45 | 46 | {truncate(publicKey, { 47 | length: 10, 48 | })} 49 | 50 | {balance && 51 | 52 |  ({Number.parseInt(balance, 10)} XLM) 53 | } 54 | 55 | 58 | 59 | 60 | const SignInModal = ({formValidate, handleOnSubmit, handleClose, showModal}) => 61 | 62 | 63 | Sign In 64 | 65 | 66 |
67 |
74 | 77 |
78 |
79 | 89 |
90 | 91 | 92 | 93 |
94 | 95 | const secretToPublicKey = secret => Keypair.fromSecret(secret).publicKey() 96 | 97 | const accountPath = (serverURL, account) => { 98 | const isTestnet = serverURL.indexOf('testnet') !== -1 99 | const domain = `${isTestnet ? 'testnet.' : ''}steexp.com` 100 | return `https://${domain}/account/${account}` 101 | } 102 | 103 | class SignIn extends React.Component { 104 | state = {showModal: false} 105 | 106 | componentDidMount() { 107 | if (this.props.signer && this.props.signer != null) 108 | this.props.server 109 | .loadAccount(secretToPublicKey(this.props.signer)) 110 | .then(acc => { 111 | this.setState({balanceXLM: acc.balances[0].balance}) 112 | }) 113 | .catch(err => { 114 | console.error( 115 | `Failed to loadAccount for signer [${this.props 116 | .signer}]: ${err.message}; stack: ${err.stack}` 117 | ) 118 | alert( 119 | 'Failed to load your signing account. Check it exists on this network.' 120 | ) 121 | }) 122 | } 123 | 124 | formValidate = (formData, errors) => { 125 | if (!StrKey.isValidEd25519SecretSeed(formData.signingKey)) { 126 | errors.signingKey.addError( 127 | 'Signing key is not a valid ed25519 secret seed' 128 | ) 129 | } 130 | return errors 131 | } 132 | 133 | handleClose = () => { 134 | this.setState({showModal: false}) 135 | } 136 | 137 | handleOnSignOut = () => { 138 | this.props.onUnSetSigner() 139 | } 140 | 141 | handleOnSubmit = ({formData}) => { 142 | this.props.onSetSigner(formData.signingKey) 143 | this.handleClose() 144 | } 145 | 146 | handleOpen = () => { 147 | this.setState({showModal: true}) 148 | } 149 | 150 | render() { 151 | const publicKey = 152 | this.props.signer && this.props.signer != null 153 | ? secretToPublicKey(this.props.signer) 154 | : undefined 155 | 156 | return ( 157 |
158 | {!publicKey && } 159 | {publicKey && 160 | } 169 | 175 |
176 | ) 177 | } 178 | } 179 | 180 | SignIn.propTypes = { 181 | onSetSigner: PropTypes.func.isRequired, 182 | onUnSetSigner: PropTypes.func.isRequired, 183 | server: PropTypes.object.isRequired, 184 | signer: PropTypes.string, 185 | } 186 | 187 | export default withServer(withSigner(SignIn)) 188 | -------------------------------------------------------------------------------- /src/components/contracts/JointAccountBase.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Form from 'react-jsonschema-form' 3 | import {Col, Grid, Row} from 'react-bootstrap' 4 | import sdk from 'stellar-sdk' 5 | 6 | import Contracts from '../../api' 7 | import AccountWithHelpersField from '../fields/AccountWithHelpersField' 8 | import SignerWithWeightField from '../fields/SignerWithWeightField' 9 | import CreateButton from '../CreateButton' 10 | import Receipt from '../Receipt' 11 | import {isSignedIn, withServer, withSigner} from '../../utils' 12 | 13 | const schema = { 14 | title: 'Joint Account', 15 | description: 16 | "1) Enter Secret Key of an existing account OR 2) 'Generate' new account OR 3) 'Use Signer'", 17 | type: 'object', 18 | properties: { 19 | jointAccount: { 20 | title: 'Account', 21 | type: 'object', 22 | }, 23 | members: { 24 | title: 'Members', 25 | type: 'array', 26 | items: { 27 | type: 'object', 28 | properties: { 29 | publicKey: { 30 | type: 'string', 31 | default: '', 32 | }, 33 | weight: { 34 | type: 'integer', 35 | default: 1, 36 | }, 37 | }, 38 | }, 39 | }, 40 | thresholds: { 41 | title: 'Thresholds', 42 | type: 'object', 43 | properties: { 44 | high: { 45 | title: 'High', 46 | type: 'integer', 47 | default: 0, 48 | }, 49 | med: { 50 | title: 'Medium', 51 | type: 'integer', 52 | default: 0, 53 | }, 54 | low: { 55 | title: 'Low', 56 | type: 'integer', 57 | default: 0, 58 | }, 59 | masterWeight: { 60 | title: 'Master Weight', 61 | type: 'integer', 62 | default: 0, 63 | }, 64 | }, 65 | }, 66 | signer: {type: 'string'}, 67 | }, 68 | } 69 | const uiSchema = { 70 | jointAccount: { 71 | 'ui:field': 'account', 72 | 'ui:placeholder': 'Secret key of joint account ', 73 | }, 74 | members: { 75 | 'ui:options': { 76 | orderable: false, 77 | }, 78 | 'ui:help': 79 | "Enter stellar public keys for all accounts to add as signers. Click the '+' button to add more.", 80 | items: { 81 | 'ui:field': 'memberAccount', 82 | publicKey: { 83 | 'ui:placeholder': 'Public key of member account', 84 | }, 85 | }, 86 | }, 87 | thresholds: { 88 | 'ui:help': 'Apply joint account thresholds ...', 89 | }, 90 | signer: { 91 | 'ui:widget': 'hidden', 92 | }, 93 | } 94 | 95 | const fields = { 96 | account: AccountWithHelpersField, 97 | memberAccount: SignerWithWeightField, 98 | } 99 | 100 | class JointAccountCustom extends React.Component { 101 | state = {isLoading: false} 102 | 103 | constructor(props) { 104 | super(props) 105 | this.formData = this.props.formData 106 | this.formData.signer = props.signer ? props.signer : '' 107 | } 108 | 109 | componentWillReceiveProps(nextProps) { 110 | if (this.props.signer !== nextProps.signer) 111 | this.formData.signer = nextProps.signer 112 | } 113 | 114 | formValidate = (formData, errors) => { 115 | console.log(`Validate: ${JSON.stringify(formData)}`) 116 | 117 | // check user is signed in as we need a tx signer 118 | if (!isSignedIn(formData)) 119 | errors.signer.addError( 120 | "You must be signed in to create this contract. Click 'Sign In' in the page header." 121 | ) 122 | 123 | if (formData.members.length < 1) { 124 | errors.members.addError( 125 | "Provide at least 1 member account. Click '+' to add a member field." 126 | ) 127 | } 128 | 129 | formData.members.forEach((member, idx) => { 130 | if (!sdk.StrKey.isValidEd25519PublicKey(member.publicKey)) { 131 | errors.members[idx].addError( 132 | 'Account is not a valid ed25519 public key' 133 | ) 134 | } 135 | // TODO: check account exists on the network too .. (server.loadAccount) 136 | }) 137 | 138 | return errors 139 | } 140 | 141 | handleOnSubmit = ({formData}) => { 142 | this.setState({error: null, isLoading: true}) 143 | const contracts = new Contracts(this.props.server) 144 | const contract = contracts.jointAccount() 145 | contract 146 | .create({ 147 | accountSecret: formData.jointAccount.secretKey, 148 | members: formData.members, 149 | signerSecret: this.props.signer, 150 | }) 151 | .then(receipt => { 152 | console.log(JSON.stringify(receipt)) 153 | this.setState({isLoading: false, receipt: receipt}) 154 | }) 155 | .catch(err => { 156 | console.error(`create failed:`) 157 | console.error(err) 158 | this.setState({ 159 | isLoading: false, 160 | error: err.detail ? err.detail : err.message, 161 | }) 162 | }) 163 | } 164 | 165 | render() { 166 | return ( 167 | 168 | 169 | 170 |
179 | 187 | 188 | {this.state.receipt && } 189 | 190 | 191 | {} 192 | 193 |
194 |
195 | ) 196 | } 197 | } 198 | 199 | export default withServer(withSigner(JointAccountCustom)) 200 | -------------------------------------------------------------------------------- /src/components/contracts/Token.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Form from 'react-jsonschema-form' 3 | import {Col, Grid, Panel, Row} from 'react-bootstrap' 4 | import sdk from 'stellar-sdk' 5 | import _ from 'lodash' 6 | 7 | import Contracts from '../../api' 8 | import Receipt from '../Receipt' 9 | import CreateButton from '../CreateButton' 10 | import {isSignedIn, withServer, withSigner} from '../../utils' 11 | 12 | const schema = { 13 | title: 'Token - Issue a New Token', 14 | type: 'object', 15 | properties: { 16 | assetDetails: { 17 | title: 'Asset Details', 18 | type: 'object', 19 | required: ['assetCode', 'numOfTokens'], 20 | properties: { 21 | assetCode: { 22 | type: 'string', 23 | title: 'Asset Code', 24 | maxLength: 12, 25 | }, 26 | numOfTokens: { 27 | type: 'integer', 28 | title: 'Number of Tokens', 29 | }, 30 | }, 31 | }, 32 | accounts: { 33 | title: 'Accounts (optional)', 34 | type: 'object', 35 | properties: { 36 | issuingAccountKey: { 37 | type: 'string', 38 | title: 'Issuing Account Signing Key', 39 | }, 40 | distAccountKey: { 41 | type: 'string', 42 | title: 'Distribution Account Signing Key', 43 | }, 44 | }, 45 | }, 46 | limitFlag: { 47 | title: 'Limit Supply (optional)', 48 | type: 'object', 49 | properties: { 50 | limit: { 51 | type: 'boolean', 52 | title: 'Limit [if checked the supply will be fixed forever]', 53 | default: false, 54 | }, 55 | }, 56 | }, 57 | signer: {type: 'string'}, 58 | }, 59 | } 60 | 61 | // add this to the description : . See details on supported formats here.", 62 | const uiSchema = { 63 | assetDetails: { 64 | assetCode: { 65 | 'ui:description': 66 | "Codes are alphanumeric strings up to 12 characters in length. See 'Issuing Assets' link for full details.", 67 | 'ui:placeholder': 'eg. BEAN', 68 | }, 69 | numOfTokens: { 70 | 'ui:placeholder': 'Number of tokens to Issue', 71 | }, 72 | }, 73 | accounts: { 74 | issuingAccountKey: { 75 | 'ui:help': 'Leave blank to have a new account created', 76 | 'ui:placeholder': 'Issuing account signing key (Optional)', 77 | }, 78 | distAccountKey: { 79 | 'ui:help': 'Leave blank to have a new account created', 80 | 'ui:placeholder': 'Distribution account signing key (Optional)', 81 | }, 82 | }, 83 | signer: { 84 | 'ui:widget': 'hidden', 85 | }, 86 | } 87 | 88 | const HelpPanel = () => ( 89 | 90 |
Creates a new token on the Stellar Network.
91 |
92 | This contract mirrors the setup described in the 'Tokens on Stellar' 93 | article (link below). Check it out for full details. 94 |
95 |
96 | To create, simply enter an Asset Code and Number of Tokens to issue. An 97 | issuing and distribution account will be created for you and the keys for 98 | these will be included in the contract receipt. 99 |
100 |
101 | However if you have account(s) setup already for these roles then enter 102 | the signing keys for these so they can be configured for the token. 103 |
104 |
105 | References: 106 |
107 | 112 | 117 | 122 |
123 |
124 |
125 | ) 126 | 127 | class Token extends React.Component { 128 | formData = {} 129 | state = {isLoading: false} 130 | 131 | constructor(props) { 132 | super(props) 133 | this.formData.signer = props.signer ? props.signer : '' 134 | } 135 | 136 | componentWillReceiveProps(nextProps) { 137 | if (this.props.signer !== nextProps.signer) 138 | this.formData.signer = nextProps.signer 139 | } 140 | 141 | formValidate(formData, errors) { 142 | console.log(`Validate: ${JSON.stringify(formData)}`) 143 | 144 | // check user is signed in as we need a tx signer 145 | if (!isSignedIn(formData)) 146 | errors.signer.addError( 147 | "You must be signed in to create this contract. Click 'Sign In' in the page header." 148 | ) 149 | 150 | const accs = formData.accounts 151 | 152 | const signKeyFields = ['issuingAccountKey', 'distAccountKey'] 153 | signKeyFields.forEach(signKeyField => { 154 | if ( 155 | !_.isEmpty(accs[signKeyField]) && 156 | !sdk.StrKey.isValidEd25519SecretSeed(accs[signKeyField]) 157 | ) { 158 | errors.accounts[signKeyField].addError( 159 | 'Signing key is not a valid ed25519 secret seed' 160 | ) 161 | } 162 | }) 163 | 164 | return errors 165 | } 166 | 167 | handleOnSubmit = ({formData}) => { 168 | this.setState({error: null, isLoading: true}) 169 | 170 | const tokenOpts = { 171 | ...formData.accounts, 172 | ...formData.assetDetails, 173 | ...formData.limitFlag, 174 | signer: formData.signer, 175 | } 176 | 177 | const contracts = new Contracts(this.props.server) 178 | const tokenContract = contracts.token() 179 | tokenContract 180 | .create(tokenOpts) 181 | .then(receipt => { 182 | console.log(JSON.stringify(receipt)) 183 | this.setState({isLoading: false, receipt: receipt}) 184 | }) 185 | .catch(err => { 186 | console.error(`create failed:`) 187 | console.error(err) 188 | this.setState({ 189 | isLoading: false, 190 | error: err.detail ? err.detail : err.message, 191 | }) 192 | }) 193 | } 194 | 195 | render() { 196 | return ( 197 | 198 | 199 | 200 |
207 | 215 | 216 | {this.state.receipt && } 217 | 218 | 219 | 220 | 221 |
222 |
223 | ) 224 | } 225 | } 226 | 227 | export default withServer(withSigner(Token)) 228 | -------------------------------------------------------------------------------- /src/api/token.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Issue a token on stellar as described in the blog post: https://www.stellar.org/blog/tokens-on-stellar/ 3 | */ 4 | 5 | import {keypairReadable} from '../utils' 6 | 7 | const loadBalances = (server, publicKey) => 8 | server.loadAccount(publicKey).then(acc => { 9 | return acc.balances 10 | }) 11 | 12 | const loadThresholds = (server, publicKey) => 13 | server.loadAccount(publicKey).then(acc => { 14 | return {thresholds: acc.thresholds, masterWeight: acc.signers[0].weight} 15 | }) 16 | 17 | // Generate a doc template something like the example in Step 5 of Tokens on Stellar. 18 | // Move some of these fields to the input form if generating this is useful enough. 19 | const docTemplate = code => { 20 | return { 21 | about: 22 | "Example of a doc file for a token. You could fill this out, sign and publish it to ipfs as described in Step 5 of 'Tokens on Stellar' article.", 23 | code: code, 24 | name: `${code} token`, 25 | description: `The ${code} token ...`, 26 | conditions: 'Enter some conditions of token use here ...', 27 | } 28 | } 29 | 30 | const createAccountOperation = (sdk, publicKey, startingBalance) => { 31 | const operation = sdk.Operation.createAccount({ 32 | destination: publicKey, 33 | startingBalance: String(startingBalance), // api expects a string for the balance 34 | }) 35 | return operation 36 | } 37 | 38 | const createTokenAccounts = ( 39 | sdk, 40 | server, 41 | signingAccount, 42 | signingKeypair, 43 | issuingAccountKey, 44 | distAccountKey 45 | ) => { 46 | const txBuilder = new sdk.TransactionBuilder(signingAccount) 47 | 48 | let issuing 49 | if (!issuingAccountKey) { 50 | issuing = sdk.Keypair.random() 51 | txBuilder.addOperation(createAccountOperation(sdk, issuing.publicKey(), 31)) 52 | } 53 | 54 | let dist 55 | if (!distAccountKey) { 56 | dist = sdk.Keypair.random() 57 | txBuilder.addOperation(createAccountOperation(sdk, dist.publicKey(), 41)) 58 | } 59 | 60 | const tx = txBuilder.build() 61 | tx.sign(signingKeypair) 62 | return server 63 | .submitTransaction(tx) 64 | .then(txResult => { 65 | console.log( 66 | `issueKeypair: ${issuing && issuing.secret()} distKeypair: ${dist && 67 | dist.secret()}` 68 | ) 69 | return { 70 | issuing, 71 | dist, 72 | txResult, 73 | } 74 | }) 75 | .catch(err => { 76 | console.error(JSON.stringify(err)) 77 | throw new Error(err) 78 | }) 79 | } 80 | 81 | const trustIssuingAccount = async ( 82 | sdk, 83 | server, 84 | distAccountKeypair, 85 | asset, 86 | numOfTokens 87 | ) => { 88 | const opts = { 89 | asset: asset, 90 | limit: String(numOfTokens), // trust for the full amount 91 | } 92 | 93 | const distAccount = await server.loadAccount(distAccountKeypair.publicKey()) 94 | const txBuilder = new sdk.TransactionBuilder(distAccount) 95 | txBuilder.addOperation(sdk.Operation.changeTrust(opts)) 96 | 97 | const tx = txBuilder.build() 98 | tx.sign(distAccountKeypair) 99 | 100 | return server.submitTransaction(tx).then(res => res).catch(err => { 101 | console.error(JSON.stringify(err)) 102 | throw new Error(err) 103 | }) 104 | } 105 | 106 | const createTokens = async ( 107 | sdk, 108 | server, 109 | issuingAccountKeypair, 110 | distAccount, 111 | asset, 112 | numOfTokens 113 | ) => { 114 | const opts = { 115 | asset: asset, 116 | destination: distAccount, 117 | amount: String(numOfTokens), // trust for the full amount 118 | } 119 | 120 | const issuingAccount = await server.loadAccount( 121 | issuingAccountKeypair.publicKey() 122 | ) 123 | const txBuilder = new sdk.TransactionBuilder(issuingAccount) 124 | txBuilder.addOperation(sdk.Operation.payment(opts)) 125 | 126 | const tx = txBuilder.build() 127 | tx.sign(issuingAccountKeypair) 128 | 129 | return server.submitTransaction(tx).then(res => res).catch(err => { 130 | console.error(JSON.stringify(err)) 131 | throw new Error(err) 132 | }) 133 | } 134 | 135 | const limitSupply = async (sdk, server, issuingAccountKeypair) => { 136 | const issuingAccount = await server.loadAccount( 137 | issuingAccountKeypair.publicKey() 138 | ) 139 | const txBuilder = new sdk.TransactionBuilder(issuingAccount) 140 | txBuilder.addOperation( 141 | sdk.Operation.setOptions({ 142 | masterWeight: 0, 143 | lowThreshold: 1, 144 | medThreshold: 1, 145 | highThreshold: 1, 146 | }) 147 | ) 148 | const tx = txBuilder.build() 149 | tx.sign(issuingAccountKeypair) 150 | 151 | return server.submitTransaction(tx).then(res => res).catch(err => { 152 | console.error(JSON.stringify(err)) 153 | throw new Error(err) 154 | }) 155 | } 156 | 157 | class Token { 158 | constructor(sdk, server) { 159 | this.sdk = sdk 160 | this.server = server 161 | } 162 | 163 | async create(props) { 164 | console.log(`Create Token: ${JSON.stringify(props)}`) 165 | 166 | // 167 | // Create new accounts for issuing and/or distribution if not provided 168 | // 169 | 170 | const {assetCode, limit, numOfTokens, signer} = props 171 | let {issuingAccountKey, distAccountKey} = props 172 | let createAccountsResponse 173 | if (!issuingAccountKey || !distAccountKey) { 174 | const signingKeypair = this.sdk.Keypair.fromSecret(signer) 175 | const signingAccount = await this.server.loadAccount( 176 | signingKeypair.publicKey() 177 | ) 178 | const {issuing, dist, txResult} = await createTokenAccounts( 179 | this.sdk, 180 | this.server, 181 | signingAccount, 182 | signingKeypair, 183 | issuingAccountKey, 184 | distAccountKey 185 | ) 186 | if (issuing) issuingAccountKey = issuing.secret() 187 | if (dist) distAccountKey = dist.secret() 188 | createAccountsResponse = txResult 189 | } 190 | 191 | const issuingAccountKeypair = this.sdk.Keypair.fromSecret(issuingAccountKey) 192 | const distAccountKeypair = this.sdk.Keypair.fromSecret(distAccountKey) 193 | console.log(`issuingAccountKeypair: ${issuingAccountKeypair.publicKey()}`) 194 | console.log(`distAccountKeypair: ${distAccountKeypair.publicKey()}`) 195 | 196 | // 197 | // Add trustline from distribution account to issuing account 198 | // 199 | 200 | const asset = new this.sdk.Asset( 201 | assetCode, 202 | issuingAccountKeypair.publicKey() 203 | ) 204 | const trustIssuingResponse = await trustIssuingAccount( 205 | this.sdk, 206 | this.server, 207 | distAccountKeypair, 208 | asset, 209 | numOfTokens 210 | ) 211 | console.log(`trustIssuing res=${JSON.stringify(trustIssuingResponse)}`) 212 | 213 | // 214 | // Create tokens by sending them from issuer to distributer 215 | // 216 | 217 | const createTokensResponse = await createTokens( 218 | this.sdk, 219 | this.server, 220 | issuingAccountKeypair, 221 | distAccountKeypair.publicKey(), 222 | asset, 223 | numOfTokens 224 | ) 225 | console.log(`createTokens res=${JSON.stringify(createTokensResponse)}`) 226 | 227 | // 228 | // Limit supply 229 | // 230 | 231 | console.log(`limit: ${limit} type: ${typeof limit}`) 232 | let limitSupplyResponse 233 | if (limit === true) { 234 | limitSupplyResponse = await limitSupply( 235 | this.sdk, 236 | this.server, 237 | issuingAccountKeypair 238 | ) 239 | } 240 | console.log( 241 | `limitSupplyResponse res=${JSON.stringify(limitSupplyResponse)}` 242 | ) 243 | 244 | const issuingBalances = await loadBalances( 245 | this.server, 246 | issuingAccountKeypair.publicKey() 247 | ) 248 | const distBalances = await loadBalances( 249 | this.server, 250 | distAccountKeypair.publicKey() 251 | ) 252 | // grab these to show if the supply was locked or not 253 | const issuingThresholds = await loadThresholds( 254 | this.server, 255 | issuingAccountKeypair.publicKey() 256 | ) 257 | 258 | return { 259 | accounts: { 260 | distribution: { 261 | keys: keypairReadable(distAccountKeypair), 262 | balances: distBalances, 263 | }, 264 | issuing: { 265 | keys: keypairReadable(issuingAccountKeypair), 266 | balances: issuingBalances, 267 | thresholds: issuingThresholds, 268 | }, 269 | }, 270 | docTemplate: docTemplate(props.assetCode), 271 | transactions: { 272 | createTokens: createTokensResponse, 273 | trustIssuing: trustIssuingResponse, 274 | createAccounts: createAccountsResponse ? createAccountsResponse : null, 275 | limitSupply: limitSupplyResponse ? limitSupplyResponse : null, 276 | }, 277 | inputs: props, 278 | } 279 | } 280 | } 281 | 282 | export default Token 283 | -------------------------------------------------------------------------------- /src/components/contracts/ROSCARotatedSavings.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Form from 'react-jsonschema-form' 3 | import {Col, Grid, Panel, Row} from 'react-bootstrap' 4 | import sdk from 'stellar-sdk' 5 | 6 | import Contracts from '../../api' 7 | import CreateButton from '../CreateButton' 8 | import Receipt from '../Receipt' 9 | import {isSignedIn, withServer, withSigner} from '../../utils' 10 | 11 | const schema = { 12 | title: 'ROSCA - Rotated Savings', 13 | type: 'object', 14 | required: ['startDate'], 15 | properties: { 16 | currency: { 17 | title: 'Currency / Asset', 18 | type: 'object', 19 | required: ['assetCode', 'assetIssuer', 'depositAmount'], 20 | properties: { 21 | assetCode: { 22 | title: 'Asset Code', 23 | type: 'string', 24 | maxLength: 12, 25 | }, 26 | assetIssuer: { 27 | title: 'Asset Issuer (Public Key)', 28 | type: 'string', 29 | }, 30 | depositAmount: { 31 | title: 'Daily Deposit (each individual)', 32 | type: 'integer', 33 | }, 34 | }, 35 | }, 36 | members: { 37 | title: 'Members', 38 | type: 'array', 39 | minItems: 3, 40 | items: { 41 | type: 'string', 42 | default: '', 43 | }, 44 | }, 45 | startDate: { 46 | title: 'Start Date', 47 | format: 'date', 48 | type: 'string', 49 | }, 50 | signer: {type: 'string'}, 51 | }, 52 | } 53 | const uiSchema = { 54 | currency: { 55 | depositAmount: { 56 | 'ui:help': 57 | 'Amount of the asset required to be deposited each day by each individual.', 58 | }, 59 | }, 60 | members: { 61 | 'ui:options': { 62 | orderable: true, 63 | }, 64 | 'ui:help': 65 | "Enter stellar public keys for all accounts in the ROSCA. Accounts should have trustlines to the asset issuer already. Click the '+' button to add more. Change the order using the up and down arrows.", 66 | items: { 67 | 'ui:placeholder': 'Public key of member account', 68 | }, 69 | }, 70 | startDate: { 71 | 'ui:widget': 'alt-date', 72 | 'ui:help': 73 | 'Payments will start on this day and so the first payout will also be on this day.', 74 | }, 75 | signer: { 76 | 'ui:widget': 'hidden', 77 | }, 78 | } 79 | 80 | // preseed with some testdata 81 | // TODO: remove this after testing ... 82 | const formDataInitial = { 83 | currency: { 84 | assetCode: 'KHR', 85 | assetIssuer: 'GBWINCFVJ3YOYEKXRSKI5M4I4X7QEPXCZ242NOAYPQV3XMTDHLBFKYEO', 86 | depositAmount: 10000, 87 | }, 88 | members: [ 89 | 'GDHZHBSLDWOBFUJ7KZOF4CLO5POOEPOCMWAKUIK37HL4QCJINIRH5Z2G', 90 | 'GBXLY27M5EKPJ7QLLXOWRDMTVLCOKVYPZLB6EUQP7T4MM4F6RTKG3PRZ', 91 | 'GCUVQ3ADUBUTX6627V27HEZJOCGPSAFQKQBZ5QAPVVK7TQU5CEMG5N6K', 92 | ], 93 | } 94 | 95 | const headSpace = {marginTop: '1em'} 96 | 97 | const HelpPanel = () => 98 | 99 |
100 | A contract setup for ROSCA Rotated Savings or 'Merry-go-round' savings 101 | clubs. 102 |
103 |
104 | Pay outs follow a simple fixed order determined when the contract is 105 | created. So no bids are made for the payout each round. 106 |
107 |
108 | NOTE: ideally each member is running a wallet that takes deposits and 109 | coordinates payment of the payout each round. This contract creation just 110 | demonstrates how the scheme might be setup intially. 111 |
112 |
113 |
114 |

Asset Issuer

115 |
116 |
117 | Select an issuer for the deposit asset. Typically this will be an issuer 118 | for the local currency. 119 |
120 |
121 | It might be simplest for the group to setup thier own issuer account or 122 | designate a deposit collector account as the issuer here. 123 |
124 |
125 | Alternatively if all members already have currency with a local provider 126 | on the Stellar network then it might make sense to set that provider 127 | here. 128 |
129 |
130 |
131 |
132 |

Trustline from Members to Asset Issuer

133 |
134 | All members need a trustline in Stellar to the asset issuer before 135 | submitting this form. An error message will be displayed on create if 136 | accounts don't have trust. 137 |
138 |
139 |
140 |

Payout Order

141 |
142 | Payout order is fixed and reflects the order on the form. So group should 143 | agree no order BEFORE contract creation. Use the up and down arrows next 144 | to the member account fields to change the order. 145 |
146 |
147 |
148 |

Timing

149 |
150 | Select Start Date of the ROSCA. The first deposits and payout will be on 151 | this day. The last day of the round will be 'N members' days after the 152 | start date. 153 |
154 |
155 |
156 |

References

157 |
158 |
159 |
160 | 161 | The Poor and Their Money 162 | {' '} 163 | (p.14 'The Merry-go-round') 164 |
165 | 170 | 175 |
176 |
177 |
178 | 179 | class ROSCARotatedSavings extends React.Component { 180 | state = {isLoading: false} 181 | formData = formDataInitial 182 | 183 | constructor(props) { 184 | super(props) 185 | this.formData.signer = props.signer ? props.signer : '' 186 | } 187 | 188 | componentWillReceiveProps(nextProps) { 189 | if (this.props.signer !== nextProps.signer) 190 | this.formData.signer = nextProps.signer 191 | } 192 | 193 | formValidate = (formData, errors) => { 194 | console.log(`Validate: ${JSON.stringify(formData)}`) 195 | 196 | // check user is signed in as we need a tx signer 197 | if (!isSignedIn(formData)) 198 | errors.signer.addError( 199 | "You must be signed in to create this contract. Click 'Sign In' in the page header." 200 | ) 201 | 202 | if (formData.currency.depositAmount < 1) { 203 | errors.currency.depositAmount.addError( 204 | 'Deposit amounts must be at least 1.' 205 | ) 206 | } 207 | 208 | if (!sdk.StrKey.isValidEd25519PublicKey(formData.currency.assetIssuer)) { 209 | errors.currency.assetIssuer.addError( 210 | 'Account is not a valid ed25519 public key' 211 | ) 212 | } 213 | 214 | if (formData.members.length < 3) { 215 | errors.members.addError( 216 | "Provide at least 3 member accounts. Click '+' to add a member field." 217 | ) 218 | } 219 | 220 | formData.members.forEach((member, idx) => { 221 | if (!sdk.StrKey.isValidEd25519PublicKey(member)) { 222 | errors.members[idx].addError( 223 | 'Account is not a valid ed25519 public key' 224 | ) 225 | } 226 | }) 227 | 228 | return errors 229 | } 230 | 231 | handleOnSubmit = ({formData}) => { 232 | this.setState({error: null, isLoading: true}) 233 | const contracts = new Contracts(this.props.server) 234 | const contract = contracts.roscaRotatedSavings() 235 | contract 236 | .create({ 237 | ...formData.currency, 238 | startDate: formData.startDate, 239 | members: formData.members, 240 | signerSecret: this.props.signer, 241 | }) 242 | .then(receipt => { 243 | console.log(JSON.stringify(receipt)) 244 | this.setState({isLoading: false, receipt: receipt}) 245 | }) 246 | .catch(err => { 247 | console.error(err) 248 | this.setState({ 249 | isLoading: false, 250 | error: err.detail ? err.detail : err.message, 251 | }) 252 | }) 253 | } 254 | 255 | render() { 256 | return ( 257 | 258 | 259 | 260 |
268 | 276 | 277 | {this.state.receipt && } 278 | 279 | 280 | 281 | 282 |
283 |
284 | ) 285 | } 286 | } 287 | 288 | export default withServer(withSigner(ROSCARotatedSavings)) 289 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------