├── .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 |
13 | You need to enable JavaScript to run this app.
14 |
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 |
14 | Create Contract
15 |
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 | switcher(network)}
9 | >
10 | {network.toUpperCase()}
11 |
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 |
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 | [](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 |
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 | Copy to Clipboard
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 | Close
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 |
55 | Generate
56 |
57 |
58 |
59 |
63 | this.handleOnClickUseSigner(this.props.formContext.signer)}
64 | >
65 | Use Signer
66 |
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 |
37 | Generate
38 |
39 |
40 |
45 |
54 | Use Signer
55 |
56 |
57 |
,
58 | ]
59 | `;
60 |
61 | exports[`renders with minimal fields 1`] = `
62 | Array [
63 |
67 |
72 |
80 |
81 |
86 |
95 | Generate
96 |
97 |
98 |
103 |
112 | Use Signer
113 |
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 |
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 |
81 |
82 | Menu
83 |
84 |
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 |
32 | Sign In
33 |
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 |
56 | Sign Out
57 |
58 |
59 |
60 | const SignInModal = ({formValidate, handleOnSubmit, handleClose, showModal}) =>
61 |
62 |
63 | Sign In
64 |
65 |
66 |
67 |
78 |
79 |
89 |
90 |
91 | Close
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------