├── src ├── storage.js ├── wallet.js ├── clients │ ├── node.js │ ├── browser.js │ └── index.js ├── storage │ ├── base-storage.js │ ├── in-memory-storage.js │ ├── local-storage.js │ └── file-storage.js ├── __tests__ │ ├── in-memory-storage.test.js │ ├── file-storage.test.js │ ├── ecies-ephemeral.test.js │ ├── simple-wallet.test.js │ └── exports.test.js ├── wallet │ ├── base-wallet.js │ └── simple-wallet.js ├── index.js ├── build-transaction.js └── crypto.js ├── .prettierrc ├── .npmignore ├── .gitmodules ├── post ├── docs ├── storage.md ├── wallet.md ├── cli.md └── library.md ├── package.json ├── examples └── moneybutton.html ├── README.md ├── cli └── .gitignore /src/storage.js: -------------------------------------------------------------------------------- 1 | const FileStorage = require('./storage/file-storage'); 2 | module.exports = FileStorage; 3 | -------------------------------------------------------------------------------- /src/wallet.js: -------------------------------------------------------------------------------- 1 | const SimpleWallet = require('./wallet/simple-wallet'); 2 | module.exports = SimpleWallet; 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "parser": "flow", 4 | "useTabs": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .bit 3 | .git 4 | .gitmodules 5 | .prettierrc 6 | bsvabi 7 | coverage 8 | dosc/ 9 | examples 10 | scripts 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "bsvabi"] 2 | path = bsvabi 3 | url = https://github.com/twetch-inc/bsvabi.git 4 | [submodule "shared-helpers"] 5 | path = shared-helpers 6 | url = https://github.com/twetch-inc/shared-helpers.git 7 | -------------------------------------------------------------------------------- /post: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const Twetch = require('./dist/twetch.node.min.js'); 4 | const twetch = new Twetch(); 5 | 6 | console.log('Twetch SDK CLI'); 7 | 8 | (async () => { 9 | await twetch.publish('twetch/post@0.0.1', { 10 | bContent: process.argv[2] 11 | }); 12 | process.exit(); 13 | })(); 14 | -------------------------------------------------------------------------------- /src/clients/node.js: -------------------------------------------------------------------------------- 1 | const Client = require('./index'); 2 | const FileStorage = require('../storage/file-storage'); 3 | 4 | class NodeClient extends Client { 5 | constructor(options = {}) { 6 | const Storage = options.Storage || FileStorage; 7 | super({ ...options, Storage }); 8 | } 9 | } 10 | 11 | module.exports = NodeClient; 12 | -------------------------------------------------------------------------------- /src/clients/browser.js: -------------------------------------------------------------------------------- 1 | const Client = require('./index'); 2 | const LocalStorage = require('../storage/local-storage'); 3 | 4 | class BrowserClient extends Client { 5 | constructor(options = {}) { 6 | const Storage = options.Storage || LocalStorage; 7 | super({ ...options, Storage }); 8 | } 9 | } 10 | 11 | module.exports = BrowserClient; 12 | -------------------------------------------------------------------------------- /src/storage/base-storage.js: -------------------------------------------------------------------------------- 1 | class BaseStorage { 2 | setItem(key, value) { 3 | throw new Error('you must implement `setItem`'); 4 | } 5 | 6 | getItem(key) { 7 | throw new Error('you must implement `getItem`'); 8 | } 9 | 10 | removeItem(key) { 11 | throw new Error('you must implement `removeItem`'); 12 | } 13 | } 14 | 15 | module.exports = BaseStorage; 16 | -------------------------------------------------------------------------------- /src/__tests__/in-memory-storage.test.js: -------------------------------------------------------------------------------- 1 | const InMemoryStorage = require('../storage/in-memory-storage'); 2 | const storage = new InMemoryStorage(); 3 | 4 | const key = 'foo'; 5 | const value = 'bar'; 6 | 7 | test('read, write, remove', () => { 8 | storage.setItem(key, value); 9 | expect(storage.getItem(key)).toBe(value); 10 | storage.removeItem(key); 11 | expect(storage.getItem(key)).toBe(undefined); 12 | }); 13 | -------------------------------------------------------------------------------- /src/storage/in-memory-storage.js: -------------------------------------------------------------------------------- 1 | const BaseStorage = require('./base-storage'); 2 | 3 | class InMemoryStorage extends BaseStorage { 4 | constructor(options = {}) { 5 | super(options); 6 | this.map = {}; 7 | } 8 | 9 | setItem(key, value) { 10 | this.map[key] = value; 11 | } 12 | 13 | getItem(key) { 14 | return this.map[key]; 15 | } 16 | 17 | removeItem(key) { 18 | delete this.map[key]; 19 | } 20 | } 21 | 22 | module.exports = InMemoryStorage; 23 | -------------------------------------------------------------------------------- /src/wallet/base-wallet.js: -------------------------------------------------------------------------------- 1 | class BaseWallet { 2 | address() { 3 | throw new Error('you must implement `address`'); 4 | } 5 | 6 | backup() { 7 | throw new Error('you must implement `backup`'); 8 | } 9 | 10 | sign(message) { 11 | throw new Error('you must implement `sign`'); 12 | } 13 | 14 | async balance() { 15 | throw new Error('you must implement `balance`'); 16 | } 17 | 18 | async buildTx(data, payees) { 19 | throw new Error('you must implement `buildTx`'); 20 | } 21 | }; 22 | 23 | module.exports = BaseWallet; 24 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | exports.BrowserClient = require('./clients/browser'); 2 | exports.Client = require('./clients/index'); 3 | exports.NodeClient = require('./clients/node'); 4 | 5 | exports.BaseStorage = require('./storage/base-storage'); 6 | exports.FileStorage = require('./storage/file-storage'); 7 | exports.InMemoryStorage = require('./storage/in-memory-storage'); 8 | exports.LocalStorage = require('./storage/local-storage'); 9 | 10 | exports.BaseWallet = require('./wallet/base-wallet'); 11 | exports.SimpleWallet = require('./wallet/simple-wallet'); 12 | -------------------------------------------------------------------------------- /src/storage/local-storage.js: -------------------------------------------------------------------------------- 1 | const BaseStorage = require('./base-storage'); 2 | const isNode = typeof window === 'undefined'; 3 | 4 | class LocalStorage extends BaseStorage { 5 | constructor(options = {}) { 6 | super(options); 7 | } 8 | 9 | setItem(key, value) { 10 | !isNode && localStorage.setItem(key, value); 11 | } 12 | 13 | getItem(key) { 14 | return !isNode && localStorage.getItem(key); 15 | } 16 | 17 | removeItem(key) { 18 | !isNode && localStorage.removeItem(key); 19 | } 20 | } 21 | 22 | module.exports = LocalStorage; 23 | -------------------------------------------------------------------------------- /src/__tests__/file-storage.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const FileStorage = require('../storage/file-storage'); 3 | const filePath = `${__dirname}/.bit-temp`; 4 | const storage = new FileStorage({ filePath }); 5 | 6 | const key = 'foo'; 7 | const value = 'bar'; 8 | 9 | test('read, write, remove', () => { 10 | storage.setItem(key, value); 11 | expect(storage.getItem(key)).toBe(value); 12 | storage.removeItem(key); 13 | expect(storage.getItem(key)).toBe(undefined); 14 | }); 15 | 16 | afterAll(() => { 17 | fs.unlinkSync(filePath); 18 | }); 19 | -------------------------------------------------------------------------------- /src/__tests__/ecies-ephemeral.test.js: -------------------------------------------------------------------------------- 1 | const TwetchCrypto = require("../crypto"); 2 | 3 | test('ephemeral ecies', () => { 4 | const msg = "hello"; 5 | const priv = "KwjURBciGgHp7iyrcdreMYM641yYbZ9TAZLSEnmmKnkMUZKyXgJv"; 6 | const pub = "02dae575a43c9fd8b9b070a65ee817e0b98087e526b30f8c83534e0b56b9d929b5"; 7 | 8 | const encrypted = TwetchCrypto.eciesEphemeralEncrypt(msg, pub); 9 | const decrypted = TwetchCrypto.eciesDecrypt(Buffer.from(encrypted.message, 'hex').toString('base64'), priv); 10 | 11 | console.log(encrypted); 12 | console.log(decrypted); 13 | 14 | expect(decrypted).toBe(msg); 15 | }); -------------------------------------------------------------------------------- /docs/storage.md: -------------------------------------------------------------------------------- 1 | # Storage Class 2 | 3 | ```javascript 4 | const Twetch = require('@twetch/sdk'); 5 | class CustomStorage { 6 | constructor(options) {} 7 | 8 | setItem(key, value) {} 9 | 10 | getItem(key) {} 11 | 12 | removeItem(key) {} 13 | } 14 | 15 | const twetch = new Twetch({ Storage: CustomStorage }); 16 | ``` 17 | 18 | ## Options 19 | 20 | An options object can have the following keys. The options object from the client will also be passed to the wallet. 21 | - `filePath` (string) - Optional. Path to file for persistant storage. Default project directory. 22 | 23 | ## Methods 24 | 25 | ### `setItem(key, value)` 26 | set a key/value pair in storage 27 | 28 | ### `getItem(key)` 29 | get a value by key from storage 30 | 31 | ### `removeItem(key)` 32 | remove a key/value pair from storage 33 | -------------------------------------------------------------------------------- /src/__tests__/simple-wallet.test.js: -------------------------------------------------------------------------------- 1 | const SimpleWallet = require('../wallet/simple-wallet'); 2 | const initialPrivateKey = 'L4yPxuPnn7Yzno7byYSebCosrt6zp9Au9QRg9KoEBzF1GXJvJpgq'; 3 | const wallet = new SimpleWallet({ privateKey: initialPrivateKey }); 4 | wallet.storage.setItem('didBackup', true); 5 | 6 | const privateKey = 'KwzP1cawZ9mhjzBmVvLTRLAFq4s8W5Dh7TZYDhumKyAgihn1bAts'; 7 | const address = '13nRpHT4QXM6XJYFhXCeDjQxoJAT5EZm7R'; 8 | const signature = 9 | 'ILPPN7Ip75qP7M67p7885ttyvzSCT8/VhEnZyqD5mOG7fVTmdzQtv6LWCOiAU+SyYp96Q9iLFcmkCfb7Ehd5x70='; 10 | 11 | test('wallet', () => { 12 | expect(wallet.privateKey.toString()).toBe(initialPrivateKey); 13 | wallet.restore(privateKey); 14 | expect(wallet.privateKey.toString()).toBe(privateKey); 15 | expect(wallet.address()).toBe(address); 16 | expect(wallet.sign('yo frawg')).toBe(signature); 17 | }); 18 | -------------------------------------------------------------------------------- /src/storage/file-storage.js: -------------------------------------------------------------------------------- 1 | const BaseStorage = require('./base-storage'); 2 | 3 | class FileStorage extends BaseStorage { 4 | constructor(options = {}) { 5 | super(options); 6 | this.filePath = options.filePath || './.bit'; 7 | } 8 | 9 | get fs() { 10 | return eval(`require('fs')`); 11 | } 12 | 13 | get file() { 14 | let file = {}; 15 | try { 16 | file = JSON.parse(this.fs.readFileSync(this.filePath).toString()); 17 | } catch (e) {} 18 | return file; 19 | } 20 | 21 | setItem(key, value) { 22 | const file = this.file; 23 | file[key] = value; 24 | this.fs.writeFileSync(this.filePath, JSON.stringify(file)); 25 | } 26 | 27 | getItem(key) { 28 | const file = this.file; 29 | return file[key]; 30 | } 31 | 32 | removeItem(key) { 33 | const file = this.file; 34 | delete file[key]; 35 | this.fs.writeFileSync(this.filePath, JSON.stringify(file)); 36 | } 37 | } 38 | 39 | module.exports = FileStorage; 40 | -------------------------------------------------------------------------------- /src/__tests__/exports.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | BrowserClient, 3 | Client, 4 | NodeClient, 5 | BaseStorage, 6 | FileStorage, 7 | InMemoryStorage, 8 | LocalStorage, 9 | BaseWallet, 10 | SimpleWallet 11 | } = require('../index'); 12 | 13 | test('imports/exports', () => { 14 | expect(new Client()).toBeInstanceOf(require('../clients/index')); 15 | expect(new BrowserClient()).toBeInstanceOf(require('../clients/browser')); 16 | expect(new NodeClient()).toBeInstanceOf(require('../clients/node')); 17 | 18 | expect(new BaseStorage()).toBeInstanceOf(require('../storage/base-storage')); 19 | expect(new FileStorage()).toBeInstanceOf(require('../storage/file-storage')); 20 | expect(new InMemoryStorage()).toBeInstanceOf(require('../storage/in-memory-storage')); 21 | expect(new LocalStorage()).toBeInstanceOf(require('../storage/local-storage')); 22 | 23 | expect(new BaseWallet()).toBeInstanceOf(require('../wallet/base-wallet')); 24 | expect(new SimpleWallet()).toBeInstanceOf(require('../wallet/simple-wallet')); 25 | }); 26 | -------------------------------------------------------------------------------- /docs/wallet.md: -------------------------------------------------------------------------------- 1 | # Wallet Class 2 | 3 | To connect any wallet to Twetch, override the default wallet class with your own implementation. 4 | Fill out these 5 functions to submit an application for listing your wallet on Twetch. 5 | 6 | ```javascript 7 | const Twetch = require('@twetch/sdk'); 8 | class CustomWallet { 9 | constructor(options = {}) {} 10 | 11 | address() {} 12 | 13 | backup() {} 14 | 15 | async balance() {} 16 | 17 | async buildTx(data, payees = [], options = {}) {} 18 | 19 | sign(message) {} 20 | } 21 | 22 | const twetch = new Twetch({ Wallet: CustomWallet }); 23 | ``` 24 | 25 | ## Options 26 | 27 | An options object can have the following keys. The options object from the client will also be passed to the wallet. 28 | 29 | - `privateKey` (string) - private key to initialize the wallet with 30 | - `feeb` (number) - Optional. Satoshis per byte in your transaction. Default 0.3. 31 | 32 | ## Methods 33 | 34 | ### `address()` 35 | returns (string) - Signing address of the wallet 36 | 37 | ### `backup()` 38 | returns (string) - A message about backing up your key 39 | 40 | ### `balance(key)` 41 | returns (number) - Balance in satoshis 42 | 43 | ### `buildTx(data, payees)` 44 | returns (string) - Signed raw transaction hex 45 | 46 | ### `sign(value)` 47 | returns (string) - Signature of the value 48 | -------------------------------------------------------------------------------- /docs/cli.md: -------------------------------------------------------------------------------- 1 | # Twetch SDK CLI 2 | 3 | ## Usage 4 | 5 | The quickest way to get started is to run the initialization command from the cli 6 | 7 | ```bash 8 | twetch init 9 | ``` 10 | 11 | After you have completed the initialization steps you can begin using the cli. To post run the following: 12 | 13 | ```bash 14 | twetch post --content "Hello World from Twetch SDK" 15 | ``` 16 | 17 | You can see additional commands and usage by running: 18 | 19 | ```bash 20 | twetch --help 21 | ``` 22 | 23 | ## Examples 24 | 25 | ### Text post 26 | 27 | ```bash 28 | twetch post --content "Hello World from Twetch SDK" 29 | ``` 30 | 31 | ### Text post with mention 32 | 33 | ```bash 34 | twetch post --content "Hello @1 from Twetch SDK" 35 | ``` 36 | 37 | ### Text post with mention and branch 38 | 39 | ```bash 40 | twetch post --content "Hello @4552 from Twetch SDK https://twetch.app/t/9ac9118692f2f0004b3de8e9ec3aad1594291135655f579b2c5b85d364edf255" 41 | ``` 42 | 43 | ### Reply 44 | 45 | ```bash 46 | twetch post --content "Hello World from Twetch SDK" --reply 9ac9118692f2f0004b3de8e9ec3aad1594291135655f579b2c5b85d364edf255 47 | ``` 48 | 49 | ### Image / Media 50 | 51 | ```bash 52 | twetch post --content "Hello World" --file file.png 53 | ``` 54 | 55 | ### Likes 56 | 57 | ```bash 58 | twetch like -t abda4a05b98a60e9098f0cccebe5948118189d1b161a0372c35fac654eb87e30 59 | ``` 60 | 61 | ### Tweet from Twetch 62 | 63 | ```bash 64 | twetch post --content "Hello Twitter from Twetch" --tweet y 65 | ``` 66 | 67 | ### Hide Twetch link from Twitter 68 | 69 | ```bash 70 | twetch post --content "Hello Twitter from Twetch" --hide y 71 | ``` 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@twetch/sdk", 3 | "version": "0.2.12", 4 | "description": "Twetch SDK", 5 | "main": "dist/twetch.node.min.js", 6 | "browser": "dist/twetch.min.js", 7 | "bin": { 8 | "twetch": "cli" 9 | }, 10 | "scripts": { 11 | "build": "npm run build-browser && npm run build-node && npm run build-in-memory", 12 | "build-in-memory": "node_modules/browserify/bin/cmd.js src/clients/index.js -o dist/twetch.in-memory.js --standalone twetchjs --node --dg --im --no-builtins && node_modules/terser/bin/terser -o dist/twetch.in-memory.min.js --module --compress --mangle -- dist/twetch.in-memory.js && rm dist/twetch.in-memory.js && rm -f dist/twetch.in-memory.js.tmp-* || true", 13 | "build-browser": "node_modules/browserify/bin/cmd.js src/clients/browser.js -o dist/twetch.js --standalone twetchjs && node_modules/terser/bin/terser -o dist/twetch.min.js --module --compress --mangle -- dist/twetch.js && rm dist/twetch.js && rm -f dist/twetch.js.tmp-* || true", 14 | "build-node": "node_modules/browserify/bin/cmd.js src/clients/node.js -o dist/twetch.node.js --standalone twetchjs --node --dg --im --no-builtins && node_modules/terser/bin/terser -o dist/twetch.node.min.js --module --compress --mangle -- dist/twetch.node.js && rm dist/twetch.node.js && rm -f dist/twetch.node.js.tmp-* || true", 15 | "dev": "nodemon --exec npm run build", 16 | "test": "jest __tests__" 17 | }, 18 | "nodemonConfig": { 19 | "ignore": [ 20 | "dist/*" 21 | ] 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+ssh://git@github.com/twetch-inc/twetch-js.git" 26 | }, 27 | "author": "", 28 | "license": "ISC", 29 | "bugs": { 30 | "url": "https://github.com/twetch-inc/twetch-js/issues" 31 | }, 32 | "homepage": "https://github.com/twetch-inc/twetch-js#readme", 33 | "dependencies": { 34 | "axios": "0.19.1", 35 | "buffer": "5.4.3", 36 | "yargs": "15.1.0" 37 | }, 38 | "devDependencies": { 39 | "browserify": "16.5.0", 40 | "chalk": "3.0.0", 41 | "jest": "25.3.0", 42 | "nodemon": "^2.0.2", 43 | "terser": "4.6.3" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/moneybutton.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Twetch SDK - MoneyButton 4 | 5 | 6 | 7 | 8 | 9 |

Twetch SDK Moneybutton Example

10 |

If you havent already, add your monebutton signing address to https://twetch.app/developer - swipe the button below to fetch your signing address.

11 |
12 |

Signing Address:

13 |

After adding your signing address you may post from moneybutton

14 |

Posting: "Hello World from Twetch SDK w/ MoneyButton"

15 |
16 | 17 | 63 | 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twetch JS SDK 2 | 3 | ## Getting Started 4 | 5 | This is a JavaScript library to interact with the Twetch API. 6 | The sdk can be used either as a library in your javascript, or as a command line interface (CLI). 7 | 8 | ## Install via NPM 9 | 10 | To install as a module for use in your own javascript project, from your project's directory run: 11 | 12 | ```bash 13 | npm install @twetch/sdk 14 | ``` 15 | 16 | To install as a CLI run: 17 | 18 | ```bash 19 | npm install -g @twetch/sdk 20 | ``` 21 | 22 | ## Authentication 23 | 24 | In order to post on Twetch you will need to let Twetch know what address you will sign your data with. 25 | To do this you can add the address as a signing address on https://twetch.app/developer. 26 | Any posts signed with your address will display on Twetch as posted by your account. One Twetch account can have 27 | many signing addresses so it's possible to use the SDK with any number of wallets. If you misplaced your key, 28 | you may revoke that signing address from the Twetch developer page. 29 | 30 | ## Wallet 31 | 32 | The sdk ships with a simple wallet, however it is designed to work with any wallet. 33 | Examples for popular wallets including Money Button, Relay One and Handcash will be created and documented. 34 | 35 | [Wallet Documentation](docs/wallet.md) 36 | 37 | ## Storage 38 | 39 | The first time you use the sdk a private key will be generated and saved into a file called `.bit` at the root of your project. 40 | To see the path of this file run `twetch storage` after initializing the sdk. 41 | 42 | [Storage Documentation](docs/storage.md) 43 | 44 | ## CLI Usage 45 | 46 | The quickest way to get started is to run the initialization command from the cli 47 | 48 | ```bash 49 | twetch init 50 | ``` 51 | 52 | After you have completed the initialization steps you can begin using the cli. To post run the following: 53 | 54 | ```bash 55 | twetch post --content "Hello World from Twetch SDK" 56 | ``` 57 | 58 | [CLI Documentation](docs/cli.md) 59 | 60 | ## Library Usage 61 | 62 | Load the module in your project 63 | 64 | ```javascript 65 | const Twetch = require('@twetch/sdk'); 66 | const twetch = new Twetch(options = {}); 67 | ``` 68 | 69 | The first time you use the library follow the instructons printed in the console by running: 70 | 71 | ```javascript 72 | twetch.init() 73 | ``` 74 | 75 | After following the instructions you may now start to use the library to interact with twetch 76 | 77 | ```javascript 78 | twetch.publish('twetch/post@0.0.1', { 79 | bContent: 'Hello World from Twetch SDK' 80 | }); 81 | ``` 82 | 83 | [Library Documentation](docs/library.md) 84 | -------------------------------------------------------------------------------- /docs/library.md: -------------------------------------------------------------------------------- 1 | # Twetch SDK Library 2 | 3 | ## Options 4 | 5 | When instantiating the sdk you can pass options to configure the instance. 6 | 7 | ```javascript 8 | const Twetch = require('@twetch/sdk'); 9 | const twetch = new Twetch(options = {}); 10 | ``` 11 | 12 | an options object can have the following keys: 13 | 14 | - `apiUrl` (string) - Optional. Default `https://api.twetch.app/v1` 15 | - `clientIdentifier` (guid) - Optional. A client identifier from https://twetch.app/developer 16 | - `network` (string) - Optional. Default `mainnet` 17 | - `Storage` (Class) - Optional. A JavaScript class which implements [Storage](docs/storage.md). Accessible after initializing via `instance.storage`. 18 | - `Wallet` (Class) - Optional. A JavaScript class which implements [Wallet](docs/wallet.md). Accessible after initializing via `instance.wallet`. 19 | 20 | ## Read Api 21 | 22 | To build your queries, visit the dashboard https://api.twetch.app/v1/graphiql. 23 | To authenticate on the dashboard copy your token from `twetch.authenticate()` and add it as a bearer token under "Headers". ex. `"Authorization": "Bearer token-goes-here"` 24 | 25 | ```javascript 26 | const token = await twetch.authenticate(); 27 | const response = await twetch.query(` 28 | query { 29 | userById(id: "1") { 30 | id 31 | name 32 | } 33 | } 34 | `) 35 | ``` 36 | 37 | ## Examples 38 | 39 | ### Text post 40 | 41 | ```javascript 42 | twetch.publish('twetch/post@0.0.1', { 43 | bContent: 'Hello World from Twetch SDK' 44 | }); 45 | ``` 46 | 47 | ### Text post with mention 48 | 49 | ```javascript 50 | // 51 | twetch.publish('twetch/post@0.0.1', { 52 | bContent: 'Hello @1 from Twetch SDK' 53 | }); 54 | ``` 55 | 56 | ### Text post with mention and branch 57 | 58 | ```javascript 59 | twetch.publish('twetch/post@0.0.1', { 60 | bContent: 'Hello @4552 from Twetch SDK https://twetch.app/t/9ac9118692f2f0004b3de8e9ec3aad1594291135655f579b2c5b85d364edf255' 61 | }); 62 | ``` 63 | 64 | ### Reply 65 | 66 | ```javascript 67 | twetch.publish('twetch/post@0.0.1', { 68 | bContent: 'Hello World from Twetch SDK', 69 | mapReply: '9ac9118692f2f0004b3de8e9ec3aad1594291135655f579b2c5b85d364edf255' 70 | }); 71 | ``` 72 | 73 | ### Image Post 74 | 75 | NOTE: "mapComment" is used for adding text instead of "bContent" when posting images/media 76 | 77 | ```javascript 78 | twetch.publish('twetch/post@0.0.1', { 79 | mapComment: 'Hello World from Twetch SDK' 80 | }, './file.png'); 81 | ``` 82 | 83 | 84 | ### Tweet From Twetch 85 | 86 | ```javascript 87 | twetch.publish('twetch/post@0.0.1', { 88 | bContent: 'test', 89 | payParams: { 90 | tweetFromTwetch: true, // optional - tweets this Twetch 91 | hideTweetFromTwetchLink: true // optional - hides Twetch link in tweet 92 | } 93 | }); 94 | ``` 95 | 96 | ### Likes 97 | 98 | ```javascript 99 | twetch.publish('twetch/like@0.0.1', { 100 | postTransaction: 'abda4a05b98a60e9098f0cccebe5948118189d1b161a0372c35fac654eb87e30' 101 | }); 102 | ``` 103 | -------------------------------------------------------------------------------- /cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const Twetch = require('./dist/twetch.node.min.js'); 4 | const twetch = new Twetch({ filePath: `${__dirname}/.bit` }); 5 | 6 | require('yargs') 7 | .scriptName('twetch') 8 | .command('init', 'to get started run this', {}, async argv => { 9 | twetch.init(); 10 | }) 11 | .command('address', 'display your wallet signing address', {}, async argv => { 12 | const address = await twetch.wallet.address(); 13 | console.log('address: ', address); 14 | process.exit(); 15 | }) 16 | .command('backup', 'backup your wallet', {}, () => { 17 | twetch.wallet.backup(); 18 | }) 19 | .command('balance', 'fetch your wallet balance', {}, async argv => { 20 | const balance = await twetch.wallet.balance(); 21 | console.log('balance: ', balance / 100000000, 'BSV'); 22 | process.exit(); 23 | }) 24 | .command( 25 | 'like', 26 | 'like a twetch post', 27 | yargs => 28 | yargs.option('t', { 29 | alias: 'transaction', 30 | describe: 'txid of the post to like' 31 | }), 32 | async argv => { 33 | const payload = { 34 | postTransaction: argv.transaction 35 | }; 36 | 37 | const response = await twetch.publish('twetch/like@0.0.1', payload); 38 | if (response.published) { 39 | console.log(`liked! https://twetch.app/t/${argv.transaction}`); 40 | } 41 | } 42 | ) 43 | .command( 44 | 'post', 45 | 'publish a message on Twetch', 46 | yargs => 47 | yargs 48 | .option('f', { 49 | alias: 'file', 50 | describe: 'path of file you want posted to twetch' 51 | }) 52 | .option('c', { 53 | alias: 'content', 54 | describe: 'content you want posted to twetch' 55 | }) 56 | .option('r', { 57 | alias: 'reply', 58 | describe: 'txid of post to reply to' 59 | }) 60 | .option('t', { 61 | alias: 'tweet', 62 | describe: 'tweet from twetch' 63 | }) 64 | .option('h', { 65 | alias: 'hide', 66 | describe: 'hide tweet from twetch link' 67 | }), 68 | async argv => { 69 | const payload = { 70 | bContent: argv.file ? undefined : argv.content, 71 | payParams: { tweetFromTwetch: argv.tweet || false, hideTweetFromTwetchLink: argv.hide || false }, 72 | mapComment: (argv.file && argv.content ? argv.content : '') || '', 73 | mapReply: argv.reply || 'null' 74 | }; 75 | 76 | const response = await twetch.publish('twetch/post@0.0.1', payload, argv.file); 77 | if (response.published) { 78 | console.log(`published! https://twetch.app/t/${response.txid}`); 79 | } 80 | process.exit(); 81 | } 82 | ) 83 | .command( 84 | 'restore', 85 | 'restore your wallet', 86 | yargs => yargs.option('k', { alias: 'key', describe: 'private key WIF' }), 87 | argv => { 88 | if (!argv.key) { 89 | return console.log('you must specify a private key'); 90 | } 91 | 92 | twetch.wallet.restore(argv.key); 93 | console.log('Wallet restored from private key'); 94 | } 95 | ) 96 | .command('storage', 'get information about persistant storage', {}, () => { 97 | console.log('path', twetch.storage.filePath); 98 | }) 99 | .help().argv; 100 | -------------------------------------------------------------------------------- /src/build-transaction.js: -------------------------------------------------------------------------------- 1 | const _Buffer = require('buffer/'); 2 | const axios = require('axios'); 3 | 4 | const PrivateKey = require('../bsvabi/bsv/lib/privatekey'); 5 | const Address = require('../bsvabi/bsv/lib/address'); 6 | const Transaction = require('../bsvabi/bsv/lib/transaction'); 7 | const Script = require('../bsvabi/bsv/lib/script'); 8 | const Opcode = require('../bsvabi/bsv/lib/opcode'); 9 | 10 | const fetchUtxos = async options => { 11 | let rpcaddr = options.pay.rpc; 12 | let key = options.pay.key; 13 | const privateKey = new PrivateKey(key); 14 | const address = privateKey.toAddress(); 15 | const response = await axios.post(`${rpcaddr}/addrs/utxo`, { 16 | addrs: [address.toString()].join(',') 17 | }); 18 | return response.data; 19 | }; 20 | 21 | const build = async function(options) { 22 | let script = null; 23 | if (options.data) { 24 | script = _script(options); 25 | } 26 | 27 | let key = options.pay.key; 28 | const privateKey = new PrivateKey(key); 29 | const address = privateKey.toAddress(); 30 | 31 | let utxos = options.pay.utxos; 32 | 33 | if (!utxos) { 34 | utxos = await fetchUtxos(options); 35 | } 36 | 37 | let tx = new Transaction(options.tx).from(utxos); 38 | 39 | if (script) { 40 | tx.addOutput(new Transaction.Output({ script: script, satoshis: 0 })); 41 | } 42 | 43 | const addresses = options.pay.to.filter(e => { 44 | try { 45 | new Address(e.address); 46 | return true; 47 | } catch { 48 | return false; 49 | } 50 | }); 51 | 52 | const scripts = options.pay.to.filter(e => { 53 | try { 54 | Script.fromASM(e.address); 55 | return true; 56 | } catch { 57 | return false; 58 | } 59 | }); 60 | 61 | addresses.forEach(e => { 62 | tx.to(e.address, e.value); 63 | }); 64 | 65 | scripts.forEach(e => { 66 | tx.addOutput(new Transaction.Output({ script: Script.fromASM(e.address), satoshis: e.value })); 67 | }); 68 | 69 | tx.fee(0).change(address); 70 | let fee = Math.ceil(tx._estimateSize() * options.pay.feeb); 71 | tx.fee(fee); 72 | 73 | if (options.forceFee) { 74 | const inputAmount = utxos.reduce((a, e) => a + e.satoshis, 0); 75 | const outputAmount = fee + options.pay.to.reduce((a, e) => a + e.value, 0); 76 | 77 | if (inputAmount - outputAmount < 0) { 78 | fee = Math.ceil(tx._estimateSize() * options.pay.feeb); 79 | tx.outputs.splice(-1, 1); // drop fee output 80 | tx.outputs.splice(-1, 1); // drop last payees output 81 | 82 | const payee = options.pay.to[options.pay.to.length - 1]; 83 | tx.to(payee.address, payee.value - fee); 84 | } 85 | } 86 | 87 | for (let i = 0; i < tx.outputs.length; i++) { 88 | if (tx.outputs[i]._satoshis > 0 && tx.outputs[i]._satoshis < 546) { 89 | tx.outputs.splice(i, 1); 90 | i--; 91 | } 92 | } 93 | 94 | return tx.sign(privateKey); 95 | }; 96 | 97 | const _script = function(options) { 98 | let s = new Script(); 99 | s.add(Opcode.OP_FALSE); 100 | s.add(Opcode.OP_RETURN); 101 | options.data.forEach(function(item) { 102 | if (item.constructor.name === 'Uint8Array') { 103 | let buffer = _Buffer.Buffer.from(item); 104 | s.add(buffer); 105 | } else if (item.constructor.name === 'ArrayBuffer') { 106 | let buffer = _Buffer.Buffer.from(item); 107 | s.add(buffer); 108 | } else if (item.constructor.name === 'Buffer') { 109 | s.add(item); 110 | } else if (typeof item === 'string') { 111 | if (/^0x/i.test(item)) { 112 | s.add(Buffer.from(item.slice(2), 'hex')); 113 | } else { 114 | s.add(Buffer.from(item)); 115 | } 116 | } else if (typeof item === 'object' && item.hasOwnProperty('op')) { 117 | s.add({ opcodenum: item.op }); 118 | } 119 | }); 120 | 121 | return s; 122 | }; 123 | 124 | exports.fetchUtxos = fetchUtxos; 125 | module.exports = build; 126 | -------------------------------------------------------------------------------- /src/wallet/simple-wallet.js: -------------------------------------------------------------------------------- 1 | const BaseWallet = require('./base-wallet'); 2 | const buildTransaction = require('../build-transaction'); 3 | const InMemoryStorage = require('../storage/in-memory-storage'); 4 | const Message = require('../../bsvabi/bsv/message'); 5 | const PrivateKey = require('../../bsvabi/bsv/lib/privatekey'); 6 | const Script = require('../../bsvabi/bsv/lib/script'); 7 | const Address = require('../../bsvabi/bsv/lib/address'); 8 | const axios = require('axios'); 9 | 10 | class SimpleWallet extends BaseWallet { 11 | constructor(options = {}) { 12 | super(options); 13 | const Storage = options.Storage || InMemoryStorage; 14 | 15 | this.storage = new Storage(options); 16 | this.feeb = options.feeb || 0.5; 17 | this.network = options.network || 'mainnet'; 18 | 19 | if (options.privateKey) { 20 | this.restore(options.privateKey); 21 | } 22 | } 23 | 24 | get privateKey() { 25 | let privateKey = this.storage.getItem(`${this.network}PrivateKey`); 26 | 27 | if (!privateKey) { 28 | privateKey = PrivateKey.fromRandom(this.network); 29 | this.storage.setItem(`${this.network}PrivateKey`, privateKey.toString()); 30 | } else { 31 | privateKey = PrivateKey.fromString(privateKey); 32 | } 33 | 34 | if (!this.didShowWarning && !this.storage.getItem('didBackup')) { 35 | this.didShowWarning = true; 36 | console.log( 37 | '\nWarning: If you loose your wallet private key, you will not be able to access the wallet.' 38 | ); 39 | console.log('The wallet included in the sdk should only be used with small amounts of bsv.'); 40 | console.log('To backup your wallet, run "twetch.wallet.backup()" or "twetch backup"'); 41 | console.log( 42 | `To restore your wallet from a private key, run 'twetch.wallet.restore("private-key-here")' or 'twetch restore -k "private-key-here"'\n` 43 | ); 44 | } 45 | 46 | return privateKey; 47 | } 48 | 49 | backup() { 50 | this.storage.setItem('didBackup', true); 51 | console.log(`\nWrite down your private key and keep it somewhere safe: "${this.privateKey}"\n`); 52 | } 53 | 54 | restore(data) { 55 | const privateKey = PrivateKey(data); 56 | this.storage.setItem(`${this.network}PrivateKey`, privateKey.toString()); 57 | } 58 | 59 | address() { 60 | return this.privateKey.toAddress().toString(); 61 | } 62 | 63 | sign(message) { 64 | return Message.sign(message, this.privateKey); 65 | } 66 | 67 | async balance() { 68 | const utxos = await this.utxos(); 69 | return utxos.reduce((a, e) => a + e.satoshis, 0); 70 | } 71 | 72 | get rpc() { 73 | return { 74 | mainnet: 'https://api.bitindex.network/api', 75 | testnet: 'https://api.bitindex.network/api/v3/test' 76 | }[this.network]; 77 | } 78 | 79 | async utxos() { 80 | const address = this.address(); 81 | //const response = await axios.post(`https://api.mattercloud.io/api/v3/main/address/utxo`, { 82 | //addrs: [address].join(',') 83 | //}); 84 | //return response.data; 85 | 86 | let { data: utxos } = await axios.get( 87 | `https://api.whatsonchain.com/v1/bsv/main/address/${address}/unspent` 88 | ); 89 | utxos = utxos 90 | .sort((a, b) => a.value - b.value) 91 | .map(e => ({ 92 | txid: e.tx_hash, 93 | vout: e.tx_pos, 94 | satoshis: e.value, 95 | script: new Script(new Address(address)).toHex() 96 | })); 97 | 98 | return utxos; 99 | } 100 | 101 | async buildTx(data, payees = [], options = {}) { 102 | const to = payees.map(e => ({ 103 | address: e.to, 104 | value: parseInt((e.amount * 100000000).toFixed(0), 10) 105 | })); 106 | 107 | const tx = await buildTransaction({ 108 | data, 109 | pay: { 110 | rpc: this.rpc, 111 | key: this.privateKey.toString(), 112 | to, 113 | feeb: this.feeb, 114 | utxos: options.utxos 115 | }, 116 | ...options 117 | }); 118 | 119 | return tx; 120 | } 121 | } 122 | 123 | module.exports = SimpleWallet; 124 | -------------------------------------------------------------------------------- /src/crypto.js: -------------------------------------------------------------------------------- 1 | const BSVABI = require('../bsvabi/bsvabi'); 2 | const twetchPublicKey = '022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01'; 3 | const ecies = require('../bsvabi/bsv/ecies'); 4 | const Crypto = require('../shared-helpers/crypto'); 5 | global.window = global; 6 | 7 | class TwetchCrypto { 8 | static aesEncrypt(plainText, key) { 9 | key = Buffer.from(key, 'hex'); 10 | return Crypto.aesEncrypt(plainText, key); 11 | } 12 | 13 | static aesDecrypt(encryptedHex, key) { 14 | key = Buffer.from(key, 'hex'); 15 | return Crypto.aesDecrypt(encryptedHex, key); 16 | } 17 | 18 | static generateAesKey(l = 32) { 19 | return BSVABI.bitcoin.crypto.Hash.sha256(BSVABI.bitcoin.PrivateKey().toBuffer()) 20 | .toString('hex') 21 | .substring(l); 22 | } 23 | 24 | static eciesEphemeralDecrypt(encryptedHex, hash) { 25 | const buf = Buffer.from(hash, 'hex'); 26 | const iV = buf.slice(0, 16); 27 | const kE = buf.slice(16, 32); 28 | 29 | return Crypto.aesCBCDecrypt(encryptedHex, kE, iV); 30 | } 31 | 32 | static eciesEphemeralEncrypt(plainText, publicKey) { 33 | const r = BSVABI.bitcoin.PrivateKey.fromRandom(); 34 | const rN = r.bn; 35 | const k = BSVABI.bitcoin.PublicKey(publicKey).point; 36 | const P = k.mul(rN); 37 | const hash = BSVABI.bitcoin.crypto.Hash.sha512(BSVABI.bitcoin.PublicKey(P).toBuffer()); 38 | const iV = hash.slice(0, 16); 39 | const kE = hash.slice(16, 32); 40 | const kM = hash.slice(32, 64); 41 | const encryptedText = Crypto.aesCBCEncrypt(plainText, kE, iV); 42 | const encryptedBytes = Buffer.from(encryptedText, 'hex'); 43 | const msgBuf = Buffer.concat([Buffer.from('BIE1'), r.publicKey.toDER(true), encryptedBytes]); 44 | const hmac = BSVABI.bitcoin.crypto.Hash.sha256hmac(msgBuf, kM); 45 | return { message: Buffer.concat([msgBuf, hmac]).toString('hex'), hash: hash.toString('hex') }; 46 | } 47 | 48 | static eciesEncrypt(plainText, publicKey) { 49 | return new ecies() 50 | .publicKey(publicKey) 51 | .encrypt(plainText) 52 | .toString('base64'); 53 | } 54 | 55 | static eciesDecrypt(encryptedHex, privateKey) { 56 | try { 57 | const priv = new BSVABI.bitcoin.PrivateKey(privateKey); 58 | const decryptedMessage = new ecies() 59 | .privateKey(priv) 60 | .decrypt(Buffer.from(encryptedHex, 'base64')) 61 | .toString(); 62 | return decryptedMessage; 63 | } catch (e) { 64 | return e.toString(); 65 | } 66 | } 67 | 68 | static ecdhEncrypt(message, priv) { 69 | const key = BSVABI.bitcoin.PrivateKey(priv); 70 | const ecdh = new BSVABI.IES({ nokey: true }).privateKey(key).publicKey(twetchPublicKey); 71 | const encrypted = ecdh.encrypt(message); 72 | return encrypted.toString('hex'); 73 | } 74 | 75 | static ecdhDecrypt(encrypted, priv, pub) { 76 | const encryptedBuffer = BSVABI.bitcoin.deps.Buffer.from(encrypted, 'hex'); 77 | const key = BSVABI.bitcoin.PrivateKey(priv); 78 | const ecdh = new BSVABI.IES({ nokey: true }).privateKey(key).publicKey(pub); 79 | const message = ecdh.decrypt(encryptedBuffer); 80 | return message.toString(); 81 | } 82 | 83 | static xpubFromMnemonic(m) { 84 | const mnemonic = BSVABI.Mnemonic.fromString(m); 85 | const xpriv = BSVABI.bitcoin.HDPrivateKey.fromSeed(mnemonic.toSeed()); 86 | const xpub = BSVABI.bitcoin.HDPublicKey.fromHDPrivateKey(xpriv); 87 | return xpub.toString(); 88 | } 89 | 90 | static privFromMnemonic(m, path) { 91 | const mnemonic = BSVABI.Mnemonic.fromString(m); 92 | const xpriv = BSVABI.bitcoin.HDPrivateKey.fromSeed(mnemonic.toSeed()); 93 | return xpriv.deriveChild(path || 'm/0/0').privateKey.toString(); 94 | } 95 | 96 | static pubFromMnemonic(m, path) { 97 | const priv = this.privFromMnemonic(m, path); 98 | return new BSVABI.bitcoin.PrivateKey(priv).toPublicKey().toString(); 99 | } 100 | 101 | static addressFromMnemonic(m, path) { 102 | const priv = this.privFromMnemonic(m, path); 103 | return new BSVABI.bitcoin.PrivateKey(priv) 104 | .toPublicKey() 105 | .toAddress() 106 | .toString(); 107 | } 108 | 109 | static generateMnemonic() { 110 | return BSVABI.Mnemonic.fromRandom().toString(); 111 | } 112 | } 113 | 114 | module.exports = TwetchCrypto; 115 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | scripts 4 | schema.json 5 | package-lock.json 6 | .bit 7 | .next 8 | .env 9 | .docker-data 10 | bus/ 11 | tape.txt 12 | .creds.json 13 | 14 | # dependencies 15 | /node_modules 16 | 17 | # testing 18 | /coverage 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | .env.local 26 | .env.development.local 27 | .env.test.local 28 | .env.production.local 29 | 30 | npm-debug.log* 31 | yarn-debug.log* 32 | yarn-error.log* 33 | 34 | # 35 | # Node.js 36 | # 37 | 38 | # Logs 39 | logs 40 | *.log 41 | 42 | # Runtime data 43 | pids 44 | *.pid 45 | *.seed 46 | 47 | # Directory for instrumented libs generated by jscoverage/JSCover 48 | lib-cov 49 | 50 | # Coverage directory used by tools like istanbul 51 | coverage 52 | 53 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 54 | .grunt 55 | 56 | # Compiled binary addons (http://nodejs.org/api/addons.html) 57 | build/Release 58 | 59 | # Dependency directory 60 | # Commenting this out is preferred by some people, see 61 | # https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 62 | node_modules 63 | 64 | # Users Environment Variables 65 | .lock-wscript 66 | 67 | 68 | # 69 | # Mac 70 | # 71 | 72 | .DS_Store 73 | .AppleDouble 74 | .LSOverride 75 | 76 | # Icon must end with two \r 77 | Icon 78 | 79 | 80 | # Thumbnails 81 | ._* 82 | 83 | # Files that might appear on external disk 84 | .Spotlight-V100 85 | .Trashes 86 | 87 | # Directories potentially created on remote AFP share 88 | .AppleDB 89 | .AppleDesktop 90 | Network Trash Folder 91 | Temporary Items 92 | .apdisk 93 | 94 | 95 | # 96 | # Windows 97 | # 98 | 99 | ndows image file caches 100 | Thumbs.db 101 | ehthumbs.db 102 | 103 | # Folder config file 104 | Desktop.ini 105 | 106 | # Recycle Bin used on file shares 107 | $RECYCLE.BIN/ 108 | 109 | # Windows Installer files 110 | *.cab 111 | *.msi 112 | *.msm 113 | *.msp 114 | 115 | # 116 | # GNU/Linux 117 | # 118 | 119 | *~ 120 | 121 | # KDE directory preferences 122 | .directory 123 | 124 | # 125 | # TortoiseGit 126 | # 127 | .tgitconfig 128 | 129 | 130 | # 131 | # Vim 132 | # 133 | 134 | [._]*.s[a-w][a-z] 135 | [._]s[a-w][a-z] 136 | *.un~ 137 | Session.vim 138 | .netrwhist 139 | *~ 140 | 141 | 142 | # 143 | # Emacs 144 | # 145 | 146 | *~ 147 | \#*\# 148 | /.emacs.desktop 149 | /.emacs.desktop.lock 150 | *.elc 151 | auto-save-list 152 | tramp 153 | .\#* 154 | 155 | # Org-mode 156 | .org-id-locations 157 | *_archive 158 | 159 | # flymake-mode 160 | *_flymake.* 161 | 162 | # eshell files 163 | /eshell/history 164 | /eshell/lastdir 165 | 166 | # elpa packages 167 | /elpa/ 168 | 169 | # reftex files 170 | *.rel 171 | 172 | # AUCTeX auto folder 173 | /auto/ 174 | 175 | 176 | # 177 | # Sublime Text 178 | # 179 | 180 | # workspace files are user-specific 181 | *.sublime-workspace 182 | 183 | # project files should be checked into the repository, unless a significant 184 | # proportion of contributors will probably not be using SublimeText 185 | # *.sublime-project 186 | 187 | #sftp configuration file 188 | sftp-config.json 189 | 190 | 191 | # 192 | # JetBrains IDEs 193 | # 194 | 195 | *.iml 196 | 197 | ## Directory-based project format: 198 | .idea/ 199 | 200 | ## File-based project format: 201 | *.ipr 202 | *.iws 203 | 204 | ## Plugin-specific files: 205 | 206 | # IntelliJ 207 | out/ 208 | 209 | # mpeltonen/sbt-idea plugin 210 | .idea_modules/ 211 | 212 | # JIRA plugin 213 | atlassian-ide-plugin.xml 214 | 215 | # Crashlytics plugin (for Android Studio and IntelliJ) 216 | com_crashlytics_export_strings.xml 217 | crashlytics.properties 218 | crashlytics-build.properties 219 | 220 | 221 | # 222 | # Visual Studio 223 | # 224 | 225 | *.sln 226 | *.*proj 227 | 228 | ## Ignore Visual Studio temporary files, build results, and 229 | ## files generated by popular Visual Studio add-ons. 230 | 231 | # User-specific files 232 | *.suo 233 | *.user 234 | *.userosscache 235 | *.sln.docstates 236 | 237 | # Build results 238 | [Dd]ebug/ 239 | [Dd]ebugPublic/ 240 | [Rr]elease/ 241 | [Rr]eleases/ 242 | x64/ 243 | x86/ 244 | bld/ 245 | [Bb]in/ 246 | [Oo]bj/ 247 | 248 | # Roslyn cache directories 249 | *.ide/ 250 | 251 | # MSTest test Results 252 | [Tt]est[Rr]esult*/ 253 | [Bb]uild[Ll]og.* 254 | 255 | #NUNIT 256 | *.VisualState.xml 257 | TestResult.xml 258 | 259 | # Build Results of an ATL Project 260 | [Dd]ebugPS/ 261 | [Rr]eleasePS/ 262 | dlldata.c 263 | 264 | *_i.c 265 | *_p.c 266 | *_i.h 267 | *.ilk 268 | *.meta 269 | *.obj 270 | *.pch 271 | *.pdb 272 | *.pgc 273 | *.pgd 274 | *.rsp 275 | *.sbr 276 | *.tlb 277 | *.tli 278 | *.tlh 279 | *.tmp 280 | *.tmp_proj 281 | *.log 282 | *.vspscc 283 | *.vssscc 284 | .builds 285 | *.pidb 286 | *.svclog 287 | *.scc 288 | 289 | # Eclipse 290 | .project 291 | 292 | # Chutzpah Test files 293 | _Chutzpah* 294 | 295 | # Visual C++ cache files 296 | ipch/ 297 | *.aps 298 | *.ncb 299 | *.opensdf 300 | *.sdf 301 | *.cachefile 302 | 303 | # Visual Studio profiler 304 | *.psess 305 | *.vsp 306 | *.vspx 307 | 308 | # TFS 2012 Local Workspace 309 | $tf/ 310 | 311 | # Guidance Automation Toolkit 312 | *.gpState 313 | 314 | # ReSharper is a .NET coding add-in 315 | _ReSharper*/ 316 | *.[Rr]e[Ss]harper 317 | *.DotSettings.user 318 | 319 | # JustCode is a .NET coding addin-in 320 | .JustCode 321 | 322 | # TeamCity is a build add-in 323 | _TeamCity* 324 | 325 | # DotCover is a Code Coverage Tool 326 | *.dotCover 327 | 328 | # NCrunch 329 | _NCrunch_* 330 | .*crunch*.local.xml 331 | 332 | # MightyMoose 333 | *.mm.* 334 | AutoTest.Net/ 335 | 336 | # Web workbench (sass) 337 | .sass-cache/ 338 | 339 | # Installshield output folder 340 | [Ee]xpress/ 341 | 342 | # DocProject is a documentation generator add-in 343 | DocProject/buildhelp/ 344 | DocProject/Help/*.HxT 345 | DocProject/Help/*.HxC 346 | DocProject/Help/*.hhc 347 | DocProject/Help/*.hhk 348 | DocProject/Help/*.hhp 349 | DocProject/Help/Html2 350 | DocProject/Help/html 351 | 352 | # Click-Once directory 353 | publish/ 354 | 355 | # Publish Web Output 356 | *.[Pp]ublish.xml 357 | *.azurePubxml 358 | # TODO: Comment the next line if you want to checkin your web deploy settings 359 | # but database connection strings (with potential passwords) will be unencrypted 360 | *.pubxml 361 | *.publishproj 362 | 363 | # NuGet Packages 364 | *.nupkg 365 | # The packages folder can be ignored because of Package Restore 366 | **/packages/* 367 | # except build/, which is used as an MSBuild target. 368 | !**/packages/build/ 369 | # If using the old MSBuild-Integrated Package Restore, uncomment this: 370 | #!**/packages/repositories.config 371 | 372 | # Windows Azure Build Output 373 | csx/ 374 | *.build.csdef 375 | 376 | # Windows Store app package directory 377 | AppPackages/ 378 | 379 | # Others 380 | sql/ 381 | *.Cache 382 | ClientBin/ 383 | [Ss]tyle[Cc]op.* 384 | ~$* 385 | *~ 386 | *.dbmdl 387 | *.dbproj.schemaview 388 | *.pfx 389 | *.publishsettings 390 | node_modules/ 391 | bower_components/ 392 | 393 | # RIA/Silverlight projects 394 | Generated_Code/ 395 | 396 | # Backup & report files from converting an old project file 397 | # to a newer Visual Studio version. Backup files are not needed, 398 | # because we have git ;-) 399 | _UpgradeReport_Files/ 400 | Backup*/ 401 | UpgradeLog*.XML 402 | UpgradeLog*.htm 403 | 404 | # SQL Server files 405 | *.mdf 406 | *.ldf 407 | 408 | # Business Intelligence projects 409 | *.rdl.data 410 | *.bim.layout 411 | *.bim_*.settings 412 | 413 | # Microsoft Fakes 414 | FakesAssemblies/ 415 | *.sublime* 416 | 417 | notes.txt 418 | -------------------------------------------------------------------------------- /src/clients/index.js: -------------------------------------------------------------------------------- 1 | const BSVABI = require('../../bsvabi/bsvabi'); 2 | const axios = require('axios'); 3 | const InMemoryStorage = require('../storage/in-memory-storage'); 4 | const SimpleWallet = require('../wallet/simple-wallet'); 5 | const AuthApi = require('../../shared-helpers/auth-api'); 6 | const Helpers = require('../../shared-helpers/index'); 7 | const crypto = require('../crypto'); 8 | 9 | class Client { 10 | constructor(options = {}) { 11 | const Storage = options.Storage || InMemoryStorage; 12 | const Wallet = options.Wallet || SimpleWallet; 13 | 14 | this.options = options; 15 | this.storage = new Storage(options); 16 | this.wallet = new Wallet({ ...options, Storage }); 17 | this.clientIdentifier = options.clientIdentifier || 'e4c86c79-3eec-4069-a25c-8436ba8c6009'; 18 | this.network = options.network || 'mainnet'; 19 | this.client = axios.create({ 20 | baseURL: options.apiUrl || 'https://api.twetch.app/v1', 21 | headers: { 22 | Authorization: `Bearer ${this.storage.getItem('tokenTwetchAuth')}` 23 | } 24 | }); 25 | this.initAbi(); 26 | } 27 | 28 | get Helpers() { 29 | return Helpers; 30 | } 31 | 32 | get BSVABI() { 33 | return BSVABI; 34 | } 35 | 36 | get crypto() { 37 | return crypto; 38 | } 39 | 40 | async createMnemonic() { 41 | const mnemonic = this.crypto.generateMnemonic(); 42 | return this.syncPublicKeys(mnemonic); 43 | } 44 | 45 | async syncPublicKeys(mnemonic) { 46 | const priv = this.crypto.privFromMnemonic(mnemonic); 47 | const pub = this.crypto.pubFromMnemonic(mnemonic); 48 | 49 | let { me } = await this.me(); 50 | let publicKey = me && me.publicKey; 51 | let publicKeys = me && me.publicKeys; 52 | 53 | if (!publicKeys) { 54 | return; 55 | } 56 | 57 | if (publicKey && publicKey !== pub) { 58 | return; // seed changed 59 | } 60 | 61 | publicKeys = publicKeys.nodes.filter( 62 | e => 63 | !e.encryptedMnemonic && 64 | e.address && 65 | e.address.includes('@') && 66 | !['handcash', 'TwetchWallet'].includes(e.walletType) 67 | ); 68 | 69 | for (let each of publicKeys) { 70 | let data = mnemonic; 71 | 72 | let identityPublicKey = each.identityPublicKey; 73 | 74 | if (!identityPublicKey) { 75 | let url; 76 | 77 | if (each.address.includes('relayx.io')) { 78 | url = 'https://relayx.io/bsvalias/id/'; 79 | } 80 | 81 | if (each.address.includes('moneybutton.com')) { 82 | url = 'https://moneybutton.com/api/v1/bsvalias/id/'; 83 | } 84 | 85 | const { data: bsvalias } = await axios.get( 86 | `https://cloud-functions.twetch.app/api/bsvalias?address=${each.address}` 87 | ); 88 | 89 | identityPublicKey = bsvalias.pubkey; 90 | } 91 | 92 | if (each.walletType === 'onebutton') { 93 | data = `1harryntQnTKu5RGajGokZGqP2v8mZKJm::${data}`; 94 | } 95 | 96 | const encryptedMnemonic = this.crypto.eciesEncrypt(data, identityPublicKey); 97 | await this.updatePublicKey(each.id, { encryptedMnemonic, identityPublicKey }); 98 | } 99 | 100 | if (!publicKey) { 101 | await this.updateMe({ publicKey: pub }); 102 | } 103 | 104 | if (!me.xpub && pub === publicKey) { 105 | const xpub = this.crypto.xpubFromMnemonic(mnemonic); 106 | await this.updateMe({ xpub }); 107 | } 108 | 109 | return mnemonic; 110 | } 111 | 112 | async authenticate(options = {}) { 113 | let token = this.storage.getItem('tokenTwetchAuth'); 114 | 115 | if (!this.authenticated) { 116 | const authApi = new AuthApi(); 117 | const message = await authApi.challenge(); 118 | const signature = this.wallet.sign(message); 119 | const address = this.wallet.address(); 120 | token = await authApi.authenticate({ message, signature, address, v2: !!options.create }); 121 | } 122 | 123 | this.storage.setItem('tokenTwetchAuth', token); 124 | this.client = axios.create({ 125 | baseURL: this.options.apiUrl || 'https://api.twetch.app/v1', 126 | headers: { 127 | Authorization: `Bearer ${this.storage.getItem('tokenTwetchAuth')}` 128 | } 129 | }); 130 | this.authenticated = true; 131 | return token; 132 | } 133 | 134 | async query(query, variables = {}) { 135 | const response = await this.client.post('/graphql', { 136 | variables, 137 | query 138 | }); 139 | 140 | return response.data.data; 141 | } 142 | 143 | me() { 144 | return this.query(` 145 | query { 146 | me { 147 | id 148 | name 149 | publicKey 150 | xpub 151 | defaultWallet 152 | publicKeys: publicKeysByUserId(filter: { revokedAt: { isNull: true } }) { 153 | nodes { 154 | id 155 | walletType 156 | signingAddress 157 | identityPublicKey 158 | encryptedMnemonic 159 | address 160 | } 161 | } 162 | } 163 | } 164 | `); 165 | } 166 | 167 | async updateMe(payload) { 168 | const { me } = await this.me(); 169 | return this.query( 170 | ` 171 | mutation updateUser($payload: UserPatch!, $id: BigInt!) { 172 | updateUserById(input: {userPatch: $payload, id: $id}) { 173 | clientMutationId 174 | } 175 | } 176 | `, 177 | { payload, id: me.id } 178 | ); 179 | } 180 | 181 | async updatePublicKey(id, payload) { 182 | return this.query( 183 | ` 184 | mutation updatePublicKey($payload: PublicKeyPatch!, $id: UUID!) { 185 | updatePublicKeyById(input: {publicKeyPatch: $payload, id: $id}) { 186 | clientMutationId 187 | } 188 | } 189 | `, 190 | { payload, id } 191 | ); 192 | } 193 | 194 | async init() { 195 | console.log( 196 | `1) copy the following to add as a signing address on https://twetch.app/developer` 197 | ); 198 | const message = 'twetch-api-rocks'; 199 | console.log('\nbsv address: ', this.wallet.address()); 200 | console.log('message: ', message); 201 | console.log('signature: ', this.wallet.sign(message)); 202 | console.log('\n'); 203 | console.log(`2) fund your address with some BSV (${this.wallet.address()})`); 204 | } 205 | 206 | async initAbi() { 207 | this.abi = JSON.parse(this.storage.getItem('abi') || '{}'); 208 | this.abi = await this.fetchABI(); 209 | this.storage.setItem('abi', JSON.stringify(this.abi)); 210 | } 211 | 212 | async publish(action, payload, file) { 213 | try { 214 | console.log('signing address: ', this.wallet.address()); 215 | 216 | const balance = await this.wallet.balance(); 217 | 218 | if (!balance) { 219 | return console.log('No Funds. Please add funds to ', this.wallet.address()); 220 | } 221 | 222 | console.log('balance: ', balance / 100000000, 'BSV'); 223 | 224 | return this.buildAndPublish(action, payload, file); 225 | } catch (e) { 226 | return handleError(e); 227 | } 228 | } 229 | 230 | async build(action, payload, file, clientIdentifier) { 231 | try { 232 | if (!this.abi || !this.abi.name) { 233 | await this.initAbi(); 234 | } 235 | 236 | const abi = new BSVABI(this.abi, { 237 | network: this.network, 238 | action 239 | }); 240 | 241 | if (file) { 242 | abi.fromFile(file); 243 | } 244 | abi.fromObject(payload); 245 | 246 | if (!this.authenticated) { 247 | await this.authenticate(); 248 | } 249 | 250 | const payeeResponse = await this.fetchPayees({ 251 | args: abi.toArray(), 252 | action, 253 | clientIdentifier 254 | }); 255 | this.invoice = payeeResponse.invoice; 256 | await abi.replace({ 257 | '#{invoice}': () => payeeResponse.invoice 258 | }); 259 | 260 | return { abi, ...payeeResponse }; 261 | } catch (e) { 262 | return handleError(e); 263 | } 264 | } 265 | 266 | async buildAndPublish(action, payload, file) { 267 | try { 268 | const { abi, payees, invoice } = await this.build(action, payload, file); 269 | await abi.replace({ 270 | '#{mySignature}': () => this.wallet.sign(abi.contentHash()), 271 | '#{myAddress}': () => this.wallet.address() 272 | }); 273 | const tx = await this.wallet.buildTx(abi.toArray(), payees, action); 274 | 275 | if (this.wallet.canPublish) { 276 | return { txid: tx.hash, abi }; 277 | } 278 | 279 | new BSVABI(this.abi, { network: this.network }).action(action).fromTx(tx.toString()); 280 | const response = await this.publishRequest({ 281 | signed_raw_tx: tx.toString(), 282 | invoice, 283 | action, 284 | payParams: payload.payParams, 285 | broadcast: true 286 | }); 287 | return { ...response, txid: tx.hash, abi }; 288 | } catch (e) { 289 | return handleError(e); 290 | } 291 | } 292 | 293 | async fetchABI() { 294 | const response = await this.client.get('/abi'); 295 | return response.data; 296 | } 297 | 298 | async fetchPayees(payload) { 299 | const clientIdentifier = Object.assign(payload.clientIdentifier || this.clientIdentifier); 300 | delete payload.clientIdentifier; 301 | const response = await this.client.post('/payees', { 302 | ...payload, 303 | client_identifier: clientIdentifier 304 | }); 305 | return response.data; 306 | } 307 | 308 | async publishRequest(payload) { 309 | try { 310 | const response = await this.client.post('/publish', payload); 311 | return response.data; 312 | } catch (e) { 313 | if (e && e.response) { 314 | throw { response: { data: e.response.data } }; 315 | } 316 | 317 | throw e; 318 | } 319 | } 320 | 321 | async bsvPrice() { 322 | const { data } = await axios.get('https://cloud-functions.twetch.app/api/exchange-rate'); 323 | return data.price; 324 | } 325 | } 326 | 327 | function handleError(e) { 328 | if (e && e.response && e.response.data) { 329 | if (e.response.data.errors) { 330 | return { error: e.response.data.errors.join(', ') }; 331 | } 332 | 333 | if (e.response.status === 401) { 334 | return { error: 'unauthenticated' }; 335 | } 336 | } else if (e.toString) { 337 | console.log(e.toString()); 338 | return { error: e.toString() }; 339 | } else { 340 | return { error: e }; 341 | console.log(e); 342 | } 343 | 344 | return { error: 'something went wrong' }; 345 | } 346 | 347 | module.exports = Client; 348 | --------------------------------------------------------------------------------