├── .yarnrc ├── src ├── common │ ├── utxobased │ │ ├── keymanager │ │ │ ├── declarations │ │ │ │ ├── bn.d.ts │ │ │ │ ├── blake-hash.d.ts │ │ │ │ ├── bs58smartcheck.d.ts │ │ │ │ └── wif-smart.d.ts │ │ │ ├── coinmapper.ts │ │ │ ├── utxopicker.ts │ │ │ ├── utxopicker │ │ │ │ ├── subtractFee.ts │ │ │ │ ├── types.ts │ │ │ │ ├── accumulative.ts │ │ │ │ └── forceUseUtxo.ts │ │ │ ├── types.ts │ │ │ ├── bip69.ts │ │ │ ├── base.ts │ │ │ └── bitcoincashUtils │ │ │ │ └── base32.ts │ │ ├── engine │ │ │ ├── constants.ts │ │ │ ├── types.ts │ │ │ ├── await-lock.ts │ │ │ ├── util │ │ │ │ └── getOwnUtxosFromTx.ts │ │ │ └── paymentRequest.ts │ │ ├── info │ │ │ ├── scriptTemplates │ │ │ │ ├── types.ts │ │ │ │ └── bitcoincashScriptTemplates.ts │ │ │ ├── commonInfo.ts │ │ │ ├── utxoPickers │ │ │ │ └── dogeUtxoPicker.ts │ │ │ ├── badcoin.ts │ │ │ ├── ravencoin.ts │ │ │ ├── eboost.ts │ │ │ ├── qtum.ts │ │ │ ├── feathercoin.ts │ │ │ ├── ufo.ts │ │ │ ├── bitcoincashtestnet.ts │ │ │ ├── bitcoingoldtestnet.ts │ │ │ ├── smartcash.ts │ │ │ ├── zcoin.ts │ │ │ ├── pivx.ts │ │ │ ├── bitcointestnet.ts │ │ │ ├── bitcointestnet4.ts │ │ │ ├── vertcoin.ts │ │ │ ├── bitcoingold.ts │ │ │ ├── all.ts │ │ │ ├── ecash.ts │ │ │ ├── dogecoin.ts │ │ │ ├── digibyte.ts │ │ │ ├── litecoin.ts │ │ │ ├── groestlcoin.ts │ │ │ ├── bitcoin.ts │ │ │ ├── dash.ts │ │ │ ├── bitcoincash.ts │ │ │ └── bitcoinsv.ts │ │ ├── db │ │ │ ├── Mutex.ts │ │ │ ├── types.ts │ │ │ ├── util │ │ │ │ └── utxo.ts │ │ │ ├── Models │ │ │ │ ├── baselet.ts │ │ │ │ └── TransactionData.ts │ │ │ └── Baselets.ts │ │ └── network │ │ │ ├── types.ts │ │ │ ├── Deferred.ts │ │ │ ├── SocketEmitter.ts │ │ │ ├── windowWS.ts │ │ │ ├── nodejsWS.ts │ │ │ └── socketQueue.ts │ ├── constants.ts │ ├── plugin │ │ ├── utils.ts │ │ ├── util │ │ │ └── maximumFeeRateCalculator.ts │ │ ├── CurrencyPlugin.ts │ │ └── Metadata.ts │ ├── fees │ │ ├── processMempoolSpaceFees.ts │ │ └── calcMinerFeePerByte.ts │ ├── upgradeMemos.ts │ └── validateMemos.ts ├── util │ ├── unixTime.ts │ ├── undefinedIfEmptyString.ts │ ├── delay.ts │ ├── typeUtil.ts │ ├── indexAtProtected.ts │ ├── cleaners.ts │ ├── filterUndefined.ts │ └── uriKeyParams.ts ├── declare-modules.d.ts ├── index.ts └── react-native.ts ├── .husky └── pre-commit ├── .travis.yml ├── ios ├── edge-currency-plugins.xcodeproj │ └── project.pbxproj ├── EdgeCurrencyPluginsModule.m └── EdgeCurrencyPluginsModule.swift ├── nodemon.json ├── android ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── app │ │ └── edge │ │ └── reactnative │ │ └── currencyplugins │ │ ├── EdgeCurrencyPluginsModule.java │ │ └── EdgeCurrencyPluginsPackage.java ├── format-java.sh └── build.gradle ├── test ├── common │ ├── utxobased │ │ ├── engine │ │ │ ├── engine.fixtures │ │ │ │ ├── bitcoinTestnet │ │ │ │ │ └── dummyData │ │ │ │ │ │ └── tables │ │ │ │ │ │ ├── lastUsedByFormatPath │ │ │ │ │ │ ├── bi.json │ │ │ │ │ │ └── config.json │ │ │ │ │ │ ├── txById │ │ │ │ │ │ ├── config.json │ │ │ │ │ │ ├── 9c.json │ │ │ │ │ │ ├── 19.json │ │ │ │ │ │ ├── 86.json │ │ │ │ │ │ ├── 99.json │ │ │ │ │ │ ├── e1.json │ │ │ │ │ │ └── c5.json │ │ │ │ │ │ ├── utxoById │ │ │ │ │ │ ├── config.json │ │ │ │ │ │ ├── 19.json │ │ │ │ │ │ ├── 86.json │ │ │ │ │ │ ├── 9c.json │ │ │ │ │ │ ├── c5.json │ │ │ │ │ │ └── e1.json │ │ │ │ │ │ ├── addressByScriptPubkey │ │ │ │ │ │ └── config.json │ │ │ │ │ │ ├── utxoIdsByScriptPubkey │ │ │ │ │ │ ├── config.json │ │ │ │ │ │ └── a914.json │ │ │ │ │ │ ├── scriptPubkeyByPath │ │ │ │ │ │ ├── config.json │ │ │ │ │ │ ├── bip49_0 │ │ │ │ │ │ │ └── 0.json │ │ │ │ │ │ └── bip49_1 │ │ │ │ │ │ │ └── 0.json │ │ │ │ │ │ ├── txIdsByDate │ │ │ │ │ │ ├── config.json │ │ │ │ │ │ └── 583.json │ │ │ │ │ │ └── txIdByBlockHeight │ │ │ │ │ │ ├── config.json │ │ │ │ │ │ └── 12.json │ │ │ │ └── index.ts │ │ │ ├── PaymentRequest.spec.ts │ │ │ └── UtxoWalletTools.spec.ts │ │ ├── keymanager │ │ │ └── coins │ │ │ │ ├── checkdatasig.spec.ts │ │ │ │ ├── cashaddr.spec.ts │ │ │ │ └── groestlcointransactiontest.spec.ts │ │ └── addressFormat │ │ │ └── addressFormatIndex │ │ │ └── addressFormat.spec.ts │ ├── plugin │ │ ├── CurrencyPlugin.fixtures │ │ │ ├── index.ts │ │ │ └── common.ts │ │ └── Metadata.spec.ts │ └── fees │ │ └── fees.spec.ts ├── unit │ └── mocha.opts └── util │ ├── objectKeys.ts │ └── testLog.ts ├── .eslintignore ├── .nycrc.json ├── .editorconfig ├── .github ├── workflows │ ├── pr-checks.yml │ └── pr-rebase.yml └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .mocharc.json ├── tsconfig.json ├── .eslintrc.json ├── edge-currency-plugins.podspec ├── edge-currency-plugins.d.ts ├── LICENSE ├── Jenkinsfile ├── patches └── altcoin-js+1.0.0.patch ├── README.md ├── docs └── currency-integration.md ├── webpack.config.js └── package.json /.yarnrc: -------------------------------------------------------------------------------- 1 | --ignore-scripts true 2 | -------------------------------------------------------------------------------- /src/common/utxobased/keymanager/declarations/bn.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'bn.js' 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run precommit 5 | -------------------------------------------------------------------------------- /src/common/utxobased/keymanager/declarations/blake-hash.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'blake-hash' 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: focal 2 | language: node_js 3 | node_js: 4 | - 18 5 | script: 6 | - yarn verify 7 | -------------------------------------------------------------------------------- /ios/edge-currency-plugins.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // Placeholder so react-native autolinking can find us 2 | -------------------------------------------------------------------------------- /src/util/unixTime.ts: -------------------------------------------------------------------------------- 1 | export const unixTime = (ts: number = Date.now()): number => 2 | Math.round(ts / 1000) 3 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts,json", 4 | "exec": "npm-run-all clean -p lib types" 5 | } 6 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/lastUsedByFormatPath/bi.json: -------------------------------------------------------------------------------- 1 | {"bip49_0":3,"bip49_1":9} -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/txById/config.json: -------------------------------------------------------------------------------- 1 | {"type":"HASH_BASE","prefixSize":2} -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/utxoById/config.json: -------------------------------------------------------------------------------- 1 | {"type":"HASH_BASE","prefixSize":2} -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /.nyc_output/ 2 | /android/src/main/assets/edge-currency-plugins/ 3 | /dist/ 4 | /coverage/ 5 | /lib/ 6 | /node_modules/ 7 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": [".ts"], 3 | "include": ["src/**/*.ts"], 4 | "require": ["sucrase/register"], 5 | "all": true 6 | } 7 | -------------------------------------------------------------------------------- /test/unit/mocha.opts: -------------------------------------------------------------------------------- 1 | --colors 2 | --reporter mocha-multi-reporters 3 | --reporter-options configFile=mocha-config.json 4 | --timeout 10000 5 | -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/addressByScriptPubkey/config.json: -------------------------------------------------------------------------------- 1 | {"type":"HASH_BASE","prefixSize":4} -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/lastUsedByFormatPath/config.json: -------------------------------------------------------------------------------- 1 | {"type":"HASH_BASE","prefixSize":2} -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/utxoIdsByScriptPubkey/config.json: -------------------------------------------------------------------------------- 1 | {"type":"HASH_BASE","prefixSize":4} -------------------------------------------------------------------------------- /ios/EdgeCurrencyPluginsModule.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface RCT_EXTERN_MODULE(EdgeCurrencyPluginsModule, NSObject) 4 | @end 5 | -------------------------------------------------------------------------------- /src/util/undefinedIfEmptyString.ts: -------------------------------------------------------------------------------- 1 | export const undefinedIfEmptyString = (str?: string): string | undefined => 2 | str !== '' ? str : undefined 3 | -------------------------------------------------------------------------------- /src/util/delay.ts: -------------------------------------------------------------------------------- 1 | export const delay = async (ms: number): Promise => 2 | await new Promise(resolve => { 3 | setTimeout(resolve, ms) 4 | }) 5 | -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/index.ts: -------------------------------------------------------------------------------- 1 | import bitcoinTestnet_tests from './bitcoinTestnet/tests' 2 | 3 | export const fixtures = [bitcoinTestnet_tests] 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /src/common/utxobased/engine/constants.ts: -------------------------------------------------------------------------------- 1 | export const BLOCKBOOK_TXS_PER_PAGE = 100 2 | 3 | export const CACHE_THROTTLE = 0.05 4 | 5 | export const MAX_CONNECTIONS = 2 6 | 7 | export const NEW_CONNECTIONS = 8 8 | -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/scriptPubkeyByPath/config.json: -------------------------------------------------------------------------------- 1 | {"type":"COUNT_BASE","bucketSize":50,"partitions":{"":{"length":0},"bip49_0":{"length":29},"bip49_1":{"length":35}}} -------------------------------------------------------------------------------- /src/declare-modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-native' { 2 | export const NativeModules: { 3 | EdgeCurrencyPluginsModule: { 4 | getConstants: () => { 5 | sourceUri: string 6 | } 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/txIdsByDate/config.json: -------------------------------------------------------------------------------- 1 | {"type":"RANGE_BASE","bucketSize":2592000,"rangeKey":"date","idKey":"txid","idPrefixLength":1,"limits":{"":{"minRange":1511921755,"maxRange":1512020289}},"sizes":{"":6}} -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/txIdByBlockHeight/config.json: -------------------------------------------------------------------------------- 1 | {"type":"RANGE_BASE","bucketSize":100000,"rangeKey":"blockHeight","idKey":"txid","idPrefixLength":1,"limits":{"":{"minRange":1249371,"maxRange":1250624}},"sizes":{"":6}} -------------------------------------------------------------------------------- /.github/workflows/pr-checks.yml: -------------------------------------------------------------------------------- 1 | name: PR Checks 2 | on: [pull_request] 3 | jobs: 4 | block-wip-pr: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2.0.0 8 | - name: Block WIP PR 9 | uses: samholmes/block-wip-pr-action@v1.2.0 10 | -------------------------------------------------------------------------------- /test/util/objectKeys.ts: -------------------------------------------------------------------------------- 1 | interface ObjectType { 2 | [key: string]: unknown 3 | } 4 | 5 | /** 6 | * A type safe way to get the keys of an object. 7 | */ 8 | export const objectKeys = (object: T): Array => { 9 | return Object.keys(object) 10 | } 11 | -------------------------------------------------------------------------------- /src/common/utxobased/keymanager/declarations/bs58smartcheck.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'bs58smartcheck' { 2 | export const encode: (buffer: Buffer | number[] | Uint8Array) => string 3 | export const decodeUnsafe: (string: string) => Buffer | undefined 4 | export const decode: (string: string) => Buffer 5 | } 6 | -------------------------------------------------------------------------------- /test/util/testLog.ts: -------------------------------------------------------------------------------- 1 | import { EdgeLog } from 'edge-core-js/types' 2 | 3 | export const noOp = (..._args: any[]): void => undefined 4 | 5 | export const testLog: EdgeLog = Object.assign((): void => undefined, { 6 | breadcrumb: noOp, 7 | crash: noOp, 8 | error: noOp, 9 | warn: noOp 10 | }) 11 | -------------------------------------------------------------------------------- /src/util/typeUtil.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Like Pick, but removed fields are optional. 3 | */ 4 | export type SoftPick = Pick & Partial 5 | 6 | /** 7 | * Like Omit, but removed fields are optional. 8 | */ 9 | export type SoftOmit = Omit & Partial> 10 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### CHANGELOG 2 | 3 | Does this branch warrant an entry to the CHANGELOG? 4 | 5 | - [ ] Yes 6 | - [ ] No 7 | 8 | ### Dependencies 9 | 10 | none 11 | 12 | ### Description 13 | 14 | none 15 | -------------------------------------------------------------------------------- /src/common/constants.ts: -------------------------------------------------------------------------------- 1 | export const INFO_SERVER_URI = 'https://info1.edge.app' 2 | 3 | // Fees 4 | export const FEES_PATH = 'fees.json' 5 | export const MAX_FEE = 999999999.0 6 | export const LOW_FEE = 2 7 | export const MAX_HIGH_DELAY = 200 8 | export const MAX_STANDARD_DELAY = 6 9 | export const MIN_STANDARD_DELAY = 2 10 | export const MIN_LOW_DELAY = 1 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build output: 2 | /.cache/ 3 | /android/*.jar 4 | /android/src/main/assets/edge-currency-plugins/ 5 | /dist/ 6 | /lib/ 7 | 8 | # Package managers: 9 | node_modules/ 10 | npm-debug.log 11 | package-lock.json 12 | yarn-error.log 13 | 14 | # Editors: 15 | .DS_Store 16 | .idea/ 17 | .vscode/ 18 | 19 | # Test results: 20 | .nyc_output 21 | /coverage/ 22 | -------------------------------------------------------------------------------- /src/common/utxobased/info/scriptTemplates/types.ts: -------------------------------------------------------------------------------- 1 | export interface ScriptTemplates { 2 | [key: string]: ScriptTemplate 3 | } 4 | 5 | export type ScriptTemplate = ( 6 | pubkey: string, 7 | optionalArgs?: OptionalScriptTemplateArgs 8 | ) => string 9 | 10 | export interface OptionalScriptTemplateArgs { 11 | cdsSig?: string 12 | cdsMsg?: string 13 | cdsPubKey?: string 14 | } 15 | -------------------------------------------------------------------------------- /src/util/indexAtProtected.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Gets an element out of an array but includes protection if the index 3 | * overflows the length of the array. 4 | **/ 5 | export const indexAtProtected = ( 6 | array: T, 7 | index: number 8 | ): T extends Array ? U : undefined => 9 | array != null ? array[Math.min(index, array.length - 1)] : undefined 10 | -------------------------------------------------------------------------------- /src/common/utxobased/keymanager/coinmapper.ts: -------------------------------------------------------------------------------- 1 | import { all } from '../info/all' 2 | import { CoinInfo } from './../../plugin/types' 3 | 4 | export function getCoinFromString(coinName: string): CoinInfo { 5 | const pluginInfo = all.find( 6 | pluginInfo => pluginInfo.coinInfo.name === coinName 7 | ) 8 | if (pluginInfo == null) { 9 | throw new Error('Could not find coin info ' + coinName) 10 | } 11 | return pluginInfo.coinInfo 12 | } 13 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": ["ts"], 3 | "spec": "test/**/*.spec.ts", 4 | "require": "sucrase/register", 5 | "colors": true, 6 | "reporter":"mocha-multi-reporters", 7 | "reporterOption": "configFile=.mocharc.json", 8 | "reporterEnabled": "mochawesome,mocha-junit-reporter", 9 | "mochaJunitReporterReporterOptions": { 10 | "mochaFile": "./coverage/junit.xml" 11 | }, 12 | "mochawesomeReporterOptions": { 13 | "reportDir": "coverage", 14 | "reportName": "mochawesome" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationDir": "lib", 5 | "emitDeclarationOnly": true, 6 | "sourceRoot": "src", 7 | "sourceMap": true, 8 | "esModuleInterop": true, 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "resolveJsonModule": true, 12 | "target": "es2017", 13 | 14 | "strict": true 15 | }, 16 | "include": ["./edge-currency-plugins.d.ts", "./src/**/*", "./test/**/*"], 17 | "exclude": ["./node_modules/**/*", "./lib/**/*"] 18 | } 19 | -------------------------------------------------------------------------------- /android/format-java.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | here=$(dirname $0) 4 | 5 | formatJava() { 6 | tool="google-java-format-1.7-all-deps.jar" 7 | url="https://github.com/google/google-java-format/releases/download/google-java-format-1.7/$tool" 8 | jar="$here/$tool" 9 | 10 | # If the tool is missing, grab it from GitHub: 11 | if [ ! -e "$jar" ]; then 12 | curl -L -o "$jar" "$url" 13 | fi 14 | 15 | echo $(find "$here/src" -name "*.java") 16 | java -jar "$jar" --replace $(find "$here/src" -name "*.java") 17 | } 18 | 19 | formatJava 20 | -------------------------------------------------------------------------------- /src/util/cleaners.ts: -------------------------------------------------------------------------------- 1 | import { asCodec, asString, Cleaner } from 'cleaners' 2 | import { base16 } from 'rfc4648' 3 | 4 | /** 5 | * A string of hex-encoded binary data. 6 | */ 7 | export const asBase16: Cleaner = asCodec( 8 | raw => base16.parse(asString(raw)), 9 | clean => base16.stringify(clean).toLowerCase() 10 | ) 11 | 12 | export function asIntegerString(raw: unknown): string { 13 | const clean = asString(raw) 14 | if (!/^\d+$/.test(clean)) { 15 | throw new Error('Expected an integer string') 16 | } 17 | return clean 18 | } 19 | -------------------------------------------------------------------------------- /src/common/plugin/utils.ts: -------------------------------------------------------------------------------- 1 | import { all as plugins } from '../utxobased/info/all' 2 | 3 | export const getFormatsForNetwork = (coinName: string): string[] => { 4 | for (const plugin of plugins) { 5 | if (plugin.coinInfo.name === coinName) return plugin.engineInfo.formats 6 | } 7 | return [] 8 | } 9 | 10 | interface GenericObject { 11 | [key: string]: T 12 | } 13 | 14 | export const removeItem = (obj: GenericObject, key: string): void => { 15 | // eslint-disable-next-line @typescript-eslint/no-dynamic-delete 16 | delete obj[key] 17 | } 18 | -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/utxoById/19.json: -------------------------------------------------------------------------------- 1 | {"19e59364daf34d97ed6584e9e978f3e2375adea9a4561a83d2066d92a010ba13_0":{"id":"19e59364daf34d97ed6584e9e978f3e2375adea9a4561a83d2066d92a010ba13_0","txid":"19e59364daf34d97ed6584e9e978f3e2375adea9a4561a83d2066d92a010ba13","vout":0,"value":"16250000","scriptPubkey":"a9142244ce86d664e85801f7eb2a56dd35afd268212587","script":"a9142244ce86d664e85801f7eb2a56dd35afd268212587","redeemScript":"0014d10b4362b030c3f3fad0c0eb5a36c96f5afd1d4a","scriptType":"p2wpkhp2sh","blockHeight":1249371,"spent":false}} -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/utxoById/86.json: -------------------------------------------------------------------------------- 1 | {"86de2d2e689bda718fd7e2a486c9c6ed371c0cee9c3c6bc905211552c02f88a5_0":{"id":"86de2d2e689bda718fd7e2a486c9c6ed371c0cee9c3c6bc905211552c02f88a5_0","txid":"86de2d2e689bda718fd7e2a486c9c6ed371c0cee9c3c6bc905211552c02f88a5","vout":0,"value":"65000000","scriptPubkey":"a91438d5c9084ddc8e7c6abb34751fb24ce84738c39d87","script":"a91438d5c9084ddc8e7c6abb34751fb24ce84738c39d87","redeemScript":"0014fe19258369c5f2fc23205075e343be0953fab38f","scriptType":"p2wpkhp2sh","blockHeight":1249371,"spent":false}} -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/utxoById/9c.json: -------------------------------------------------------------------------------- 1 | {"9c8d213abeddebe6800597a5333215d9eb90fcae710c3b95daa0b7710f72612d_0":{"id":"9c8d213abeddebe6800597a5333215d9eb90fcae710c3b95daa0b7710f72612d_0","txid":"9c8d213abeddebe6800597a5333215d9eb90fcae710c3b95daa0b7710f72612d","vout":0,"value":"32500000","scriptPubkey":"a9143e5c2d1ac6394bed008d5723a74d7a49c6d7b6e287","script":"a9143e5c2d1ac6394bed008d5723a74d7a49c6d7b6e287","redeemScript":"0014299f3268005230b9c3b9b30028b277b8df122f87","scriptType":"p2wpkhp2sh","blockHeight":1249371,"spent":false}} -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/utxoById/c5.json: -------------------------------------------------------------------------------- 1 | {"c5b4b48406155c5cbddcb0be3ffcfb42dc1b698856913bd79cd0f154fb05c8a1_2":{"id":"c5b4b48406155c5cbddcb0be3ffcfb42dc1b698856913bd79cd0f154fb05c8a1_2","txid":"c5b4b48406155c5cbddcb0be3ffcfb42dc1b698856913bd79cd0f154fb05c8a1","vout":2,"value":"128370000","scriptPubkey":"a914bf76ff07ad44824990e72be8d85aa102b8cedc1287","script":"a914bf76ff07ad44824990e72be8d85aa102b8cedc1287","redeemScript":"0014bdd2dcc97172ad03275551da42680a8a3d89a9ac","scriptType":"p2wpkhp2sh","blockHeight":1250624,"spent":false}} -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/utxoById/e1.json: -------------------------------------------------------------------------------- 1 | {"e1d703801f2f64400cc98d9a2e7b6a4bcb304ff9e718685b9932c0d46cd124b3_0":{"id":"e1d703801f2f64400cc98d9a2e7b6a4bcb304ff9e718685b9932c0d46cd124b3_0","txid":"e1d703801f2f64400cc98d9a2e7b6a4bcb304ff9e718685b9932c0d46cd124b3","vout":0,"value":"130000000","scriptPubkey":"a9147a38669dfc5147ca79d4fea0b2b78c3838ce4d1787","script":"a9147a38669dfc5147ca79d4fea0b2b78c3838ce4d1787","redeemScript":"00142e59ca4d0b9b6cff8e2663c962bfe3abe000781f","scriptType":"p2wpkhp2sh","blockHeight":1250624,"spent":false}} -------------------------------------------------------------------------------- /src/common/utxobased/db/Mutex.ts: -------------------------------------------------------------------------------- 1 | type Mutex = (callback: () => Promise) => Promise 2 | 3 | export function makeMutex(): Mutex { 4 | let busy = false 5 | const queue: Array<(value: unknown) => void> = [] 6 | return async function lock(callback: () => T | Promise): Promise { 7 | if (busy) await new Promise(resolve => queue.push(resolve)) 8 | try { 9 | busy = true 10 | return callback() 11 | } finally { 12 | busy = false 13 | const resolve = queue.shift() 14 | if (resolve != null) resolve({}) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/common/utxobased/keymanager/utxopicker.ts: -------------------------------------------------------------------------------- 1 | import { accumulative } from './utxopicker/accumulative' 2 | import { forceUseUtxo } from './utxopicker/forceUseUtxo' 3 | import { subtractFee } from './utxopicker/subtractFee' 4 | import { UtxoPickingFunc } from './utxopicker/types' 5 | export * from './utxopicker/types' 6 | 7 | export interface UtxoPicker { 8 | forceUseUtxo: UtxoPickingFunc 9 | subtractFee: UtxoPickingFunc 10 | accumulative: UtxoPickingFunc 11 | } 12 | 13 | export const utxoPicker: UtxoPicker = { 14 | forceUseUtxo, 15 | subtractFee, 16 | accumulative 17 | } 18 | -------------------------------------------------------------------------------- /src/common/utxobased/network/types.ts: -------------------------------------------------------------------------------- 1 | export interface InnerSocketCallbacks { 2 | /** 3 | * Called when the socket closes. 4 | * See rfc6455 section 7.4.1 for status codes. 5 | */ 6 | onClose: (code: number, reason: string) => void 7 | 8 | onError: (error?: Event | Error) => void 9 | onMessage: (message: string) => void 10 | onOpen: () => void 11 | } 12 | 13 | export enum ReadyState { 14 | CONNECTING, 15 | OPEN, 16 | CLOSING, 17 | CLOSED 18 | } 19 | 20 | export interface InnerSocket { 21 | readyState: ReadyState 22 | disconnect: () => void 23 | send: (message: string) => void 24 | } 25 | -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/txIdsByDate/583.json: -------------------------------------------------------------------------------- 1 | [{"txid":"19e59364daf34d97ed6584e9e978f3e2375adea9a4561a83d2066d92a010ba13","date":1511921755},{"txid":"86de2d2e689bda718fd7e2a486c9c6ed371c0cee9c3c6bc905211552c02f88a5","date":1511921755},{"txid":"99701d23ae201fafeafeafa2120f048213f4cec6030208a35b4adab98f500bf0","date":1511921755},{"txid":"9c8d213abeddebe6800597a5333215d9eb90fcae710c3b95daa0b7710f72612d","date":1511921755},{"txid":"c5b4b48406155c5cbddcb0be3ffcfb42dc1b698856913bd79cd0f154fb05c8a1","date":1512020289},{"txid":"e1d703801f2f64400cc98d9a2e7b6a4bcb304ff9e718685b9932c0d46cd124b3","date":1512020289}] -------------------------------------------------------------------------------- /src/util/filterUndefined.ts: -------------------------------------------------------------------------------- 1 | // Accepts an array of values and returns a new array of values without undefined values. 2 | export const filterUndefined = (values: Array): T[] => { 3 | return values.filter(value => value !== undefined) as T[] 4 | } 5 | 6 | export const removeUndefined = >( 7 | obj: T 8 | ): Partial => { 9 | const result: Partial = {} 10 | 11 | for (const key in obj) { 12 | if (obj[key] !== undefined) { 13 | // We need to assure TypeScript that the key indeed exists on T 14 | result[key as keyof T] = obj[key] 15 | } 16 | } 17 | 18 | return result 19 | } 20 | -------------------------------------------------------------------------------- /src/util/uriKeyParams.ts: -------------------------------------------------------------------------------- 1 | export function getUriKeyParams(uri: string): string[] { 2 | const params = uri.match(/%\{(\w|\d)+\}/g) 3 | return params ?? [] 4 | } 5 | 6 | export function replaceKeyParams( 7 | uri: string, 8 | options: { [P in T]?: string } 9 | ): string { 10 | const keyParams = getUriKeyParams(uri) 11 | 12 | for (const keyParam of keyParams) { 13 | const keyIndex = keyParam.replace(/%|{|}/g, '') as T 14 | const key: string | undefined = options[keyIndex] 15 | if (key == null) throw new Error(`Missing key for connection URI ${uri}`) 16 | uri = uri.replace(keyParam, key) 17 | } 18 | 19 | return uri 20 | } 21 | -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/txIdByBlockHeight/12.json: -------------------------------------------------------------------------------- 1 | [{"txid":"19e59364daf34d97ed6584e9e978f3e2375adea9a4561a83d2066d92a010ba13","blockHeight":1249371},{"txid":"86de2d2e689bda718fd7e2a486c9c6ed371c0cee9c3c6bc905211552c02f88a5","blockHeight":1249371},{"txid":"99701d23ae201fafeafeafa2120f048213f4cec6030208a35b4adab98f500bf0","blockHeight":1249371},{"txid":"9c8d213abeddebe6800597a5333215d9eb90fcae710c3b95daa0b7710f72612d","blockHeight":1249371},{"txid":"c5b4b48406155c5cbddcb0be3ffcfb42dc1b698856913bd79cd0f154fb05c8a1","blockHeight":1250624},{"txid":"e1d703801f2f64400cc98d9a2e7b6a4bcb304ff9e718685b9932c0d46cd124b3","blockHeight":1250624}] -------------------------------------------------------------------------------- /src/common/utxobased/info/commonInfo.ts: -------------------------------------------------------------------------------- 1 | import { EdgeCurrencyInfo, EdgeMemoOption } from 'edge-core-js/types' 2 | 3 | export const utxoCustomFeeTemplate: EdgeCurrencyInfo['customFeeTemplate'] = [ 4 | { 5 | type: 'nativeAmount', 6 | key: 'satPerByte', 7 | displayName: 'Satoshis Per Byte', 8 | displayMultiplier: '0' 9 | } 10 | ] 11 | 12 | export const utxoMemoOptions: EdgeMemoOption[] = [ 13 | { 14 | type: 'hex', 15 | hidden: true, 16 | maxBytes: 80, 17 | memoName: 'OP_RETURN' 18 | } 19 | ] 20 | 21 | // Deprecated: 22 | export const legacyMemoInfo: Partial = { 23 | memoMaxLength: 80, 24 | memoType: 'text' 25 | } 26 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { EdgeCorePluginOptions, EdgeCorePlugins } from 'edge-core-js/types' 2 | 3 | import { makeCurrencyPlugin } from './common/plugin/CurrencyPlugin' 4 | import { all } from './common/utxobased/info/all' 5 | 6 | const plugins: EdgeCorePlugins = {} 7 | 8 | for (const info of all) { 9 | plugins[info.currencyInfo.pluginId] = (options: EdgeCorePluginOptions) => 10 | makeCurrencyPlugin(options, info) 11 | } 12 | 13 | declare global { 14 | interface Window { 15 | addEdgeCorePlugins?: (plugins: EdgeCorePlugins) => void 16 | } 17 | } 18 | 19 | if (typeof window !== 'undefined') { 20 | window.addEdgeCorePlugins?.(plugins) 21 | } 22 | 23 | export default plugins 24 | -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/utxoIdsByScriptPubkey/a914.json: -------------------------------------------------------------------------------- 1 | {"a9143e5c2d1ac6394bed008d5723a74d7a49c6d7b6e287":["9c8d213abeddebe6800597a5333215d9eb90fcae710c3b95daa0b7710f72612d_0"],"a91438d5c9084ddc8e7c6abb34751fb24ce84738c39d87":["86de2d2e689bda718fd7e2a486c9c6ed371c0cee9c3c6bc905211552c02f88a5_0"],"a914bf76ff07ad44824990e72be8d85aa102b8cedc1287":["c5b4b48406155c5cbddcb0be3ffcfb42dc1b698856913bd79cd0f154fb05c8a1_2"],"a9147a38669dfc5147ca79d4fea0b2b78c3838ce4d1787":["e1d703801f2f64400cc98d9a2e7b6a4bcb304ff9e718685b9932c0d46cd124b3_0"],"a9142244ce86d664e85801f7eb2a56dd35afd268212587":["19e59364daf34d97ed6584e9e978f3e2375adea9a4561a83d2066d92a010ba13_0"]} -------------------------------------------------------------------------------- /ios/EdgeCurrencyPluginsModule.swift: -------------------------------------------------------------------------------- 1 | @objc(EdgeCurrencyPluginsModule) class EdgeCurrencyPluginsModule: NSObject { 2 | @objc static func requiresMainQueueSetup() -> Bool { return false } 3 | 4 | @objc func constantsToExport() -> NSDictionary { 5 | return ["sourceUri": sourceUri() ?? ""] 6 | } 7 | 8 | func sourceUri() -> String? { 9 | if let bundleUrl = Bundle.main.url( 10 | forResource: "edge-currency-plugins", 11 | withExtension: "bundle" 12 | ), 13 | let bundle = Bundle(url: bundleUrl), 14 | let script = bundle.url(forResource: "edge-currency-plugins", withExtension: "js") 15 | { 16 | return script.absoluteString 17 | } 18 | return nil 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/common/utxobased/keymanager/declarations/wif-smart.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'wif-smart' { 2 | // Type definitions for wif-smart 2.0.0 3 | // Project: https://github.com/SmartCash/wif 4 | 5 | export interface WIFReturn { 6 | readonly version: number 7 | readonly privateKey: Buffer 8 | readonly compressed: boolean 9 | } 10 | 11 | export function decodeRaw(buffer: Buffer, version?: number): WIFReturn 12 | export function decode(string: string, version?: number): WIFReturn 13 | 14 | export function encodeRaw( 15 | version: number, 16 | privateKey: Buffer, 17 | compressed: boolean 18 | ): Buffer 19 | 20 | export function encode( 21 | version: number, 22 | privateKey: Buffer, 23 | compressed: boolean 24 | ): string 25 | } 26 | -------------------------------------------------------------------------------- /src/react-native.ts: -------------------------------------------------------------------------------- 1 | import { MemletConfig } from 'memlet' 2 | import { NativeModules } from 'react-native' 3 | 4 | const { EdgeCurrencyPluginsModule } = NativeModules 5 | const { sourceUri } = EdgeCurrencyPluginsModule.getConstants() 6 | 7 | export const pluginUri = sourceUri 8 | export const debugUri = 'http://localhost:8084/edge-currency-plugins.js' 9 | 10 | export interface EdgeCurrencyPluginIoOptions { 11 | readonly memletConfig?: MemletConfig 12 | } 13 | 14 | export interface EdgeCurrencyPluginNativeIo { 15 | readonly memletConfig?: MemletConfig 16 | } 17 | 18 | export function makePluginIo( 19 | options: EdgeCurrencyPluginIoOptions = {} 20 | ): EdgeCurrencyPluginNativeIo { 21 | const { memletConfig } = options 22 | 23 | return { 24 | memletConfig 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/common/plugin/util/maximumFeeRateCalculator.ts: -------------------------------------------------------------------------------- 1 | import { div, mul, round } from 'biggystring' 2 | import { EdgeCurrencyInfo } from 'edge-core-js/types' 3 | 4 | /** 5 | * Calculates a maximum fee rate of 1 USD per vByte given a USD exchange rate 6 | * (usdPrice). 7 | */ 8 | export const maximumFeeRateCalculator = ( 9 | currencyInfo: EdgeCurrencyInfo, 10 | usdPrice: number 11 | ): string | undefined => { 12 | const { currencyCode, denominations } = currencyInfo 13 | const primaryDenomination = denominations.find( 14 | denom => denom.name === currencyCode 15 | ) 16 | if (primaryDenomination == null) 17 | throw new Error(`Missing primary currency denomination for ${currencyCode}`) 18 | 19 | return round( 20 | mul(div('1', String(usdPrice), 16), primaryDenomination.multiplier), 21 | 0 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /test/common/plugin/CurrencyPlugin.fixtures/index.ts: -------------------------------------------------------------------------------- 1 | import { FixtureType } from './common' 2 | import { bitcoin } from './currencies/bitcoin' 3 | import { bitcoincash } from './currencies/bitcoincash' 4 | import { bitcoinsv } from './currencies/bitcoinsv' 5 | import { digibyte } from './currencies/digibyte' 6 | import { ecash } from './currencies/ecash' 7 | import { feathercoin } from './currencies/feathercoin' 8 | import { groestlcoin } from './currencies/groestlcoin' 9 | import { litecoin } from './currencies/litecoin' 10 | import { smartcash } from './currencies/smartcash' 11 | import { zcoin } from './currencies/zcoin' 12 | 13 | export const fixtures: FixtureType[] = [ 14 | bitcoin, 15 | bitcoincash, 16 | bitcoinsv, 17 | digibyte, 18 | ecash, 19 | feathercoin, 20 | groestlcoin, 21 | litecoin, 22 | smartcash, 23 | zcoin 24 | ] 25 | -------------------------------------------------------------------------------- /src/common/utxobased/network/Deferred.ts: -------------------------------------------------------------------------------- 1 | // Wrapper class for a Promise with external resolve/reject functions 2 | // Inspired by https://stackoverflow.com/questions/26150232/resolve-javascript-promise-outside-function-scope 3 | 4 | export default class Deferred { 5 | private _resolve: (value: T) => void = () => { 6 | return 7 | } 8 | 9 | private _reject: (reason?: unknown) => void = () => { 10 | return 11 | } 12 | 13 | private readonly _promise: Promise = new Promise((resolve, reject) => { 14 | this._reject = reject 15 | this._resolve = resolve 16 | }) 17 | 18 | public get promise(): Promise { 19 | return this._promise 20 | } 21 | 22 | public resolve(value: T): void { 23 | this._resolve(value) 24 | } 25 | 26 | public reject(reason?: unknown): void { 27 | this._reject(reason) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/common/utxobased/keymanager/utxopicker/subtractFee.ts: -------------------------------------------------------------------------------- 1 | import { Output, UtxoPickerArgs, UtxoPickerResult } from './types' 2 | import * as utils from './utils' 3 | 4 | export function subtractFee(args: UtxoPickerArgs): UtxoPickerResult { 5 | const { utxos, targets, feeRate } = args 6 | 7 | const outputs: Output[] = targets.map(target => ({ 8 | ...target, 9 | script: Buffer.from(target.script, 'hex'), 10 | scriptPubkey: Buffer.from(target.script, 'hex') 11 | })) 12 | 13 | const fee = Math.ceil(feeRate * utils.transactionBytes(utxos, outputs)) 14 | targets[0].value -= fee 15 | outputs[0].value -= fee 16 | 17 | const targetValue = utils.sumOrNaN(targets) 18 | if (isNaN(targetValue) || fee > targetValue) 19 | return { inputs: utxos, fee, changeUsed: false } 20 | 21 | return { inputs: utxos, outputs, fee, changeUsed: false } 22 | } 23 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "standard-kit/prettier", 4 | "standard-kit/prettier/typescript", 5 | "standard-kit/prettier/node" 6 | ], 7 | "parserOptions": { 8 | "project": "tsconfig.json" 9 | }, 10 | "plugins": ["json-files", "simple-import-sort"], 11 | "rules": { 12 | "@typescript-eslint/no-unused-vars": [ 13 | "error", 14 | { 15 | "ignoreRestSiblings": true, 16 | "argsIgnorePattern": "^_" 17 | } 18 | ], 19 | "@typescript-eslint/restrict-template-expressions": "off", 20 | "no-unused-vars": "off", 21 | "no-useless-return": "off", 22 | "no-void": "off", 23 | "simple-import-sort/imports": "error" 24 | }, 25 | "overrides": [ 26 | { 27 | "files": "package.json", 28 | "rules": { 29 | "json-files/sort-package-json": "error" 30 | } 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /test/common/utxobased/engine/PaymentRequest.spec.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai' 2 | import { expect } from 'chai' 3 | import chaiAsPromised from 'chai-as-promised' 4 | 5 | import { getSpendTargets } from '../../../../src/common/utxobased/engine/paymentRequest' 6 | 7 | chai.should() 8 | chai.use(chaiAsPromised) 9 | 10 | describe('PaymentRequest', function () { 11 | it('calculate spend target', async function () { 12 | const outputs = [ 13 | { 14 | amount: 239200, 15 | address: 'n3YaQSkTXrpbQyAns1kQQRxsECMn9ifx5n' 16 | } 17 | ] 18 | const result = getSpendTargets(outputs) 19 | expect(result).to.eql({ 20 | nativeAmount: 239200, 21 | spendTargets: [ 22 | { 23 | publicAddress: 'n3YaQSkTXrpbQyAns1kQQRxsECMn9ifx5n', 24 | nativeAmount: '239200' 25 | } 26 | ] 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /edge-currency-plugins.podspec: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, "package.json"))) 4 | 5 | Pod::Spec.new do |s| 6 | s.name = package['name'] 7 | s.version = package['version'] 8 | s.summary = package['description'] 9 | s.homepage = package['homepage'] 10 | s.license = package['license'] 11 | s.authors = package['author'] 12 | 13 | s.platform = :ios, "9.0" 14 | s.requires_arc = true 15 | s.source = { 16 | :git => "https://github.com/EdgeApp/edge-currency-plugins.git", 17 | :tag => "v#{s.version}" 18 | } 19 | s.source_files = 20 | "ios/EdgeCurrencyPluginsModule.swift", 21 | "ios/EdgeCurrencyPluginsModule.m" 22 | 23 | s.resource_bundles = { 24 | "edge-currency-plugins" => "android/src/main/assets/edge-currency-plugins/*.js" 25 | } 26 | 27 | s.dependency "React-Core" 28 | end 29 | -------------------------------------------------------------------------------- /test/common/utxobased/keymanager/coins/checkdatasig.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { describe, it } from 'mocha' 3 | 4 | import { scriptTemplates } from '../../../../../src/common/utxobased/info/scriptTemplates/bitcoincashScriptTemplates' 5 | 6 | describe('bitcoin cash checkdatasig scripting tests', () => { 7 | const redeemScript = scriptTemplates.replayProtection( 8 | '038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508' 9 | ) 10 | it('bitcoin cash checkdatasig redeem script test', () => { 11 | expect(redeemScript).to.equal( 12 | '4630440220256c12175e809381f97637933ed6ab97737d263eaaebca6add21bced67fd12a402205ce29ecc1369d6fc1b51977ed38faaf41119e3be1d7edfafd7cfaf0b6061bd070021038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508bb21038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508ac' 13 | ) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /android/src/main/java/app/edge/reactnative/currencyplugins/EdgeCurrencyPluginsModule.java: -------------------------------------------------------------------------------- 1 | package app.edge.reactnative.currencyplugins; 2 | 3 | import com.facebook.react.bridge.ReactApplicationContext; 4 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | public class EdgeCurrencyPluginsModule extends ReactContextBaseJavaModule { 9 | EdgeCurrencyPluginsModule(ReactApplicationContext context) { 10 | super(context); 11 | } 12 | 13 | @Override 14 | public Map getConstants() { 15 | final Map constants = new HashMap<>(); 16 | constants.put( 17 | "sourceUri", 18 | "file:///android_asset/edge-currency-plugins/edge-currency-plugins.js"); 19 | return constants; 20 | } 21 | 22 | @Override 23 | public String getName() { 24 | return "EdgeCurrencyPluginsModule"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/pr-rebase.yml: -------------------------------------------------------------------------------- 1 | name: PR Rebase 2 | on: 3 | issue_comment: 4 | types: [created] 5 | jobs: 6 | rebase: 7 | name: Rebase 8 | if: >- 9 | github.event.issue.pull_request != '' && 10 | ( 11 | contains(github.event.comment.body, '/autosquash') || 12 | contains(github.event.comment.body, '/fixup') || 13 | contains(github.event.comment.body, '/rebase') 14 | ) 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout the latest code 18 | uses: actions/checkout@v3 19 | with: 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | fetch-depth: 0 # otherwise, you will fail to push refs to dest repo 22 | - name: Automatic Rebase 23 | uses: EdgeApp/rebase@changelog-resolver 24 | with: 25 | autosquash: ${{ true }} 26 | changelogResolver: ${{ true }} 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | -------------------------------------------------------------------------------- /android/src/main/java/app/edge/reactnative/currencyplugins/EdgeCurrencyPluginsPackage.java: -------------------------------------------------------------------------------- 1 | package app.edge.reactnative.currencyplugins; 2 | 3 | import androidx.annotation.NonNull; 4 | import com.facebook.react.ReactPackage; 5 | import com.facebook.react.bridge.NativeModule; 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.react.uimanager.ViewManager; 8 | import java.util.Collections; 9 | import java.util.List; 10 | 11 | public class EdgeCurrencyPluginsPackage implements ReactPackage { 12 | @NonNull 13 | @Override 14 | public List createNativeModules(@NonNull ReactApplicationContext reactContext) { 15 | return Collections.singletonList( 16 | new EdgeCurrencyPluginsModule(reactContext)); 17 | } 18 | 19 | @NonNull 20 | @Override 21 | public List createViewManagers(@NonNull ReactApplicationContext reactContext) { 22 | return Collections.emptyList(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /edge-currency-plugins.d.ts: -------------------------------------------------------------------------------- 1 | import { EdgeCorePluginOptions, EdgeCurrencyPlugin } from 'edge-core-js/types' 2 | 3 | import { 4 | EdgeCurrencyPluginIoOptions, 5 | EdgeCurrencyPluginNativeIo 6 | } from './src/react-native' 7 | 8 | /** 9 | * Add this to your `nativeIo` object on React Native, 10 | * as `{ 'edge-currency-plugins': makePluginIo() }` 11 | */ 12 | export function makePluginIo( 13 | options?: EdgeCurrencyPluginIoOptions 14 | ): EdgeCurrencyPluginNativeIo 15 | 16 | /** 17 | * Debugging-URI to use on React Native, 18 | * along with running `yarn start` in this repo. 19 | */ 20 | export const debugUri: string 21 | 22 | /* Regular URI to use on React Native. */ 23 | export const pluginUri: string 24 | 25 | type EdgeCorePluginFactory = (env: EdgeCorePluginOptions) => EdgeCurrencyPlugin 26 | 27 | /** 28 | * The Node.js default export. 29 | */ 30 | declare const plugins: { 31 | [pluginId: string]: EdgeCorePluginFactory 32 | } 33 | 34 | export default plugins 35 | -------------------------------------------------------------------------------- /src/common/utxobased/keymanager/utxopicker/types.ts: -------------------------------------------------------------------------------- 1 | import { PsbtInput } from 'bip174/src/lib/interfaces' 2 | 3 | import { ScriptTypeEnum } from '../keymanager' 4 | 5 | export type Input = UTXO 6 | export interface UTXO extends PsbtInput { 7 | hash: Buffer 8 | index: number 9 | sequence: number 10 | value: number 11 | script: Buffer 12 | scriptPubkey: Buffer 13 | scriptType: ScriptTypeEnum 14 | } 15 | 16 | export interface Output { 17 | script: Buffer 18 | scriptPubkey: Buffer 19 | value: number 20 | } 21 | 22 | export interface Target { 23 | script: string 24 | value: number 25 | } 26 | 27 | export interface UtxoPickerArgs { 28 | utxos: UTXO[] 29 | useUtxos?: UTXO[] 30 | targets: Target[] 31 | feeRate: number 32 | changeScript: string 33 | } 34 | 35 | export interface UtxoPickerResult { 36 | inputs: Input[] 37 | // Nullish outputs means fee exceeds selected inputs 38 | outputs?: Output[] 39 | changeUsed: boolean 40 | fee: number 41 | } 42 | 43 | export type UtxoPickingFunc = (args: UtxoPickerArgs) => UtxoPickerResult 44 | -------------------------------------------------------------------------------- /src/common/utxobased/network/SocketEmitter.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | 3 | export declare interface SocketEmitter { 4 | emit: ((event: SocketEvent.CONNECTION_OPEN, uri: string) => void) & 5 | (( 6 | event: SocketEvent.CONNECTION_CLOSE, 7 | uri: string, 8 | error?: Error 9 | ) => boolean) & 10 | (( 11 | event: SocketEvent.CONNECTION_TIMER, 12 | uri: string, 13 | queryTime: number 14 | ) => boolean) 15 | 16 | on: (( 17 | event: SocketEvent.CONNECTION_OPEN, 18 | listener: (uri: string) => void 19 | ) => this) & 20 | (( 21 | event: SocketEvent.CONNECTION_CLOSE, 22 | listener: (uri: string, error?: Error) => void 23 | ) => this) & 24 | (( 25 | event: SocketEvent.CONNECTION_TIMER, 26 | listener: (uri: string, queryTime: number) => void 27 | ) => this) 28 | } 29 | export class SocketEmitter extends EventEmitter {} 30 | 31 | export enum SocketEvent { 32 | CONNECTION_OPEN = 'connection:open', 33 | CONNECTION_CLOSE = 'connection:close', 34 | CONNECTION_TIMER = 'connection:timer' 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Edge (formerly Airbitz) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/txById/9c.json: -------------------------------------------------------------------------------- 1 | {"9c8d213abeddebe6800597a5333215d9eb90fcae710c3b95daa0b7710f72612d":{"txid":"9c8d213abeddebe6800597a5333215d9eb90fcae710c3b95daa0b7710f72612d","hex":"01000000000101b4c8feb9caaa9473c14b10234c3837466a7b1a95edadac6e716c1796a8ea6361010000001716001475f2fa56af605aa713b7f7dfc043065247383232ffffffff0220e9ef010000000017a9143e5c2d1ac6394bed008d5723a74d7a49c6d7b6e28758cf39e92b00000017a914d8c5d40f959ba17f43996ee0e97d2f711fa82a2b870247304402204cc76bc4fd27b3c5b03addcb203d8880b5d9fdab8f0231e24e5e54d87920e671022069392f8ad752b3b35b4791c427119ea511e67b507e35aac9ee58c495c072a9df01210295d2069f6266db09ab1dd1926f1649f5ddc09a55e38ca85637ac893b324170e700000000","blockHeight":1249371,"date":1511921755,"fees":"100000","inputs":[{"txId":"6163eaa896176c716eacaded951a7b6a4637384c23104bc17394aacab9fec8b4","outputIndex":1,"n":0,"scriptPubkey":"a914e28b1c3694da1482c426ecda8989693da9f4423487","amount":"188629073688"}],"outputs":[{"n":0,"scriptPubkey":"a9143e5c2d1ac6394bed008d5723a74d7a49c6d7b6e287","amount":"32500000"},{"n":1,"scriptPubkey":"a914d8c5d40f959ba17f43996ee0e97d2f711fa82a2b87","amount":"188596473688"}],"ourIns":[],"ourOuts":["0"],"ourAmount":"32500000"}} -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/txById/19.json: -------------------------------------------------------------------------------- 1 | {"19e59364daf34d97ed6584e9e978f3e2375adea9a4561a83d2066d92a010ba13":{"txid":"19e59364daf34d97ed6584e9e978f3e2375adea9a4561a83d2066d92a010ba13","hex":"01000000000101b2ed393f1353c54a1cc364ebf1ae71ec9289dfb565566a4c5697959926c3fb8b01000000171600143cbeea594ca16860216d7b940eae5329f52f5026ffffffff0290f4f7000000000017a9142244ce86d664e85801f7eb2a56dd35afd268212587dd2daa282c00000017a9141f3b0503f81dd72ef6854be892a425370c9d374187024830450221008e368505903906696edbb297ec3d9d42d3631dc38b7fb8154212ce10fad6a4f0022063664b7ac33a315cface6fba7bf277def8b372d68bfb27d0da5b0dd73af734c701210346bcb05ff3e4b32e824a74f9da5cf0dd6106419fcd3939b0fe22ed646fde308b00000000","blockHeight":1249371,"date":1511921755,"fees":"100000","inputs":[{"txId":"8bfbc326999597564c6a5665b5df8992ec71aef1eb64c31c4ac553133f39edb2","outputIndex":1,"n":0,"scriptPubkey":"a91420233dbf0f3c341339e61c9fcb8e83e4e0650d7a87","amount":"189677152525"}],"outputs":[{"n":0,"scriptPubkey":"a9142244ce86d664e85801f7eb2a56dd35afd268212587","amount":"16250000"},{"n":1,"scriptPubkey":"a9141f3b0503f81dd72ef6854be892a425370c9d374187","amount":"189660802525"}],"ourIns":[],"ourOuts":["0"],"ourAmount":"16250000"}} -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/txById/86.json: -------------------------------------------------------------------------------- 1 | {"86de2d2e689bda718fd7e2a486c9c6ed371c0cee9c3c6bc905211552c02f88a5":{"txid":"86de2d2e689bda718fd7e2a486c9c6ed371c0cee9c3c6bc905211552c02f88a5","hex":"01000000000101ef47bed0f91b51849bdf8aac058a516ba4dc3051c5d76866d15db8e4fdcf40e601000000171600143cd6efa7f9224fa989fd4b98b16bc0a6e9d36edbffffffff0240d2df030000000017a91438d5c9084ddc8e7c6abb34751fb24ce84738c39d873c7501f92b00000017a9143224fc9ddc74444a147eb1b8acc2942202a9bce98702483045022100a29d390e64e8710d3d6070df05037277a33cb1cba90d9d297c3ef3b6e26e0eff022045fd5180d572217758cf86d5a0b982188fe918b8cbe7a25d1018daa2b96600ce012102fcbe47219bae72b108e009ea81d588e0f43f43877d84ebc820db520bc43288a800000000","blockHeight":1249371,"date":1511921755,"fees":"100000","inputs":[{"txId":"e640cffde4b85dd16668d7c55130dca46b518a05ac8adf9b84511bf9d0be47ef","outputIndex":1,"n":0,"scriptPubkey":"a9147a3f5a973d0b1c1402e40507ca1b66afbf8a656087","amount":"188926316060"}],"outputs":[{"n":0,"scriptPubkey":"a91438d5c9084ddc8e7c6abb34751fb24ce84738c39d87","amount":"65000000"},{"n":1,"scriptPubkey":"a9143224fc9ddc74444a147eb1b8acc2942202a9bce987","amount":"188861216060"}],"ourIns":[],"ourOuts":["0"],"ourAmount":"65000000"}} -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/txById/99.json: -------------------------------------------------------------------------------- 1 | {"99701d23ae201fafeafeafa2120f048213f4cec6030208a35b4adab98f500bf0":{"txid":"99701d23ae201fafeafeafa2120f048213f4cec6030208a35b4adab98f500bf0","hex":"010000000001018bf6f8644202077235c5edf57b9e3f8a06c14677735ffac066ea18039afa14260100000017160014b7e04c29e9929f2f76ce9fa7d84bd51ef293ae56ffffffff0280a4bf070000000017a9142cee2a1dc30c73232c945eb39e389cec172178ed871983f4322c00000017a914fa2199dfe08b2a12ff9741d0f2b57afb8852a75487024830450221009bcae8a93481b0285a4d7cf316e2e4265b6aeba8511c889b4295035db923305b0220021f932ce7ea5180020f8e6d6eb778c6774ad4bdfec9a1e7e26a039a496b362301210226f468993478ebfafe505de64031f79e417d69a9bf3aa8255c0893ca0507265800000000","blockHeight":1249371,"date":1511921755,"fees":"100000","inputs":[{"txId":"2614fa9a0318ea66c0fa5f737746c1068a3f9e7bf5edc5357207024264f8f68b","outputIndex":1,"n":0,"scriptPubkey":"a914ee92fd08a6ff8857dc5284e5c583b505413aec6a87","amount":"189963546169"}],"outputs":[{"n":0,"scriptPubkey":"a9142cee2a1dc30c73232c945eb39e389cec172178ed87","amount":"130000000"},{"n":1,"scriptPubkey":"a914fa2199dfe08b2a12ff9741d0f2b57afb8852a75487","amount":"189833446169"}],"ourIns":[],"ourOuts":["0"],"ourAmount":"130000000"}} -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/txById/e1.json: -------------------------------------------------------------------------------- 1 | {"e1d703801f2f64400cc98d9a2e7b6a4bcb304ff9e718685b9932c0d46cd124b3":{"txid":"e1d703801f2f64400cc98d9a2e7b6a4bcb304ff9e718685b9932c0d46cd124b3","hex":"0100000000010182271c18483704b162f0b90c14ec92c2646f161be0329b58cc2a7934fb5a8616010000001716001486372e3239089b15fd1dc5cb3a3d378d9bebc0a9ffffffff0280a4bf070000000017a9147a38669dfc5147ca79d4fea0b2b78c3838ce4d17876ef5c1162c00000017a91400c9e5c10d226995dd5dc56fcefed5b01b2af73787024730440220782d8f646d0d5d31733d8e2b544307cd7ba8add919f0bca4d754e4943ca9b9d002202e66ed1575230b84a1932b5d6ca2ef2c49637ad5bf3e06f4e8ed220ee5505248012102b02a055c2137466026407f51b45be77e6e0e1b40422bd13bb7bd37e99b04c60f00000000","blockHeight":1250624,"date":1512020289,"fees":"100000","inputs":[{"txId":"16865afb34792acc589b32e01b166f64c292ec140cb9f062b1043748181c2782","outputIndex":1,"n":0,"scriptPubkey":"a9148210e70aa4b27ce1a7ee73c971585cbe7daec22187","amount":"189490471054"}],"outputs":[{"n":0,"scriptPubkey":"a9147a38669dfc5147ca79d4fea0b2b78c3838ce4d1787","amount":"130000000"},{"n":1,"scriptPubkey":"a91400c9e5c10d226995dd5dc56fcefed5b01b2af73787","amount":"189360371054"}],"ourIns":[],"ourOuts":["0"],"ourAmount":"130000000"}} -------------------------------------------------------------------------------- /src/common/utxobased/keymanager/types.ts: -------------------------------------------------------------------------------- 1 | import { Cleaner } from 'cleaners' 2 | import { EdgeTokenId, InsufficientFundsError } from 'edge-core-js/types' 3 | 4 | interface InsufficientFundsErrorOptsPlus { 5 | // The currency we need more of: 6 | tokenId: EdgeTokenId 7 | // If we don't have enough funds for a token send: 8 | networkFee?: string 9 | 10 | networkFeeShortage?: string 11 | } 12 | export class InsufficientFundsErrorPlus extends InsufficientFundsError { 13 | networkFeeShortage?: string 14 | constructor(opts: InsufficientFundsErrorOptsPlus) { 15 | super(opts) 16 | const { networkFeeShortage } = opts 17 | this.networkFeeShortage = networkFeeShortage 18 | } 19 | } 20 | 21 | function asMaybeError(name: string): Cleaner { 22 | return function asError(raw) { 23 | if (raw instanceof Error && raw.name === name) { 24 | const typeHack: any = raw 25 | return typeHack 26 | } 27 | } 28 | } 29 | 30 | export const asMaybeInsufficientFundsErrorPlus = asMaybeError( 31 | // Share the same name because this error type is a subtype of 32 | // InsufficientFundsError and therefore backwards compatible 33 | 'InsufficientFundsError' 34 | ) 35 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.6.0' 9 | } 10 | } 11 | 12 | apply plugin: 'com.android.library' 13 | 14 | def safeExtGet(prop, fallback) { 15 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback 16 | } 17 | 18 | def DEFAULT_COMPILE_SDK_VERSION = 28 19 | def DEFAULT_BUILD_TOOLS_VERSION = '28.0.2' 20 | def DEFAULT_MIN_SDK_VERSION = 19 21 | def DEFAULT_TARGET_SDK_VERSION = 27 22 | 23 | android { 24 | namespace = "app.edge.reactnative.currencyplugins" 25 | 26 | compileSdkVersion = safeExtGet('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION) 27 | buildToolsVersion = safeExtGet('buildToolsVersion', DEFAULT_BUILD_TOOLS_VERSION) 28 | 29 | defaultConfig { 30 | minSdkVersion = safeExtGet('minSdkVersion', DEFAULT_MIN_SDK_VERSION) 31 | targetSdkVersion = safeExtGet('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION) 32 | versionCode = 1 33 | versionName = '1.0' 34 | } 35 | lintOptions { 36 | abortOnError = false 37 | } 38 | } 39 | 40 | repositories { 41 | } 42 | 43 | dependencies { 44 | implementation 'com.facebook.react:react-native:+' 45 | } 46 | -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/txById/c5.json: -------------------------------------------------------------------------------- 1 | {"c5b4b48406155c5cbddcb0be3ffcfb42dc1b698856913bd79cd0f154fb05c8a1":{"txid":"c5b4b48406155c5cbddcb0be3ffcfb42dc1b698856913bd79cd0f154fb05c8a1","hex":"01000000000101f00b508fb9da4a5ba3080203c6cef41382040f12a2affeeaaf1f20ae231d709900000000171600143825dda06b736a302f2b5fbefb464ffa1100c8f0ffffffff03503403000000000017a9141cec736050250dbfaff4b4e1819af0907322143f87a0680600000000001600141729951afbbfbe44c227201fcf1b79dc7dc4874b50c5a6070000000017a914bf76ff07ad44824990e72be8d85aa102b8cedc128702483045022100f68fce50188407e1f99825d560376e2e7cecc425aef5a90588e6016bde1c6b1b02207346894645e072b9624c11ae8804ef734e73b0890e3c737792547dfeb7e31a2601210340117bc2cc9a38e179d9e5759fe3c64ef25c1a33db8c9daadfbfb2c40c7f303700000000","blockHeight":1250624,"date":1512020289,"fees":"1000000","inputs":[{"txId":"99701d23ae201fafeafeafa2120f048213f4cec6030208a35b4adab98f500bf0","outputIndex":-1,"n":0,"scriptPubkey":"a9142cee2a1dc30c73232c945eb39e389cec172178ed87","amount":"130000000"}],"outputs":[{"n":0,"scriptPubkey":"a9141cec736050250dbfaff4b4e1819af0907322143f87","amount":"210000"},{"n":1,"scriptPubkey":"00141729951afbbfbe44c227201fcf1b79dc7dc4874b","amount":"420000"},{"n":2,"scriptPubkey":"a914bf76ff07ad44824990e72be8d85aa102b8cedc1287","amount":"128370000"}],"ourIns":["0"],"ourOuts":["2"],"ourAmount":"-1630000"}} -------------------------------------------------------------------------------- /src/common/utxobased/info/utxoPickers/dogeUtxoPicker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UtxoPicker, 3 | UtxoPickerArgs, 4 | UtxoPickerResult 5 | } from '../../keymanager/utxopicker' 6 | import { accumulative } from '../../keymanager/utxopicker/accumulative' 7 | import { forceUseUtxo } from '../../keymanager/utxopicker/forceUseUtxo' 8 | import { subtractFee } from '../../keymanager/utxopicker/subtractFee' 9 | 10 | export function makeDogeUtxoPicker(): UtxoPicker { 11 | /* 12 | According to https://github.com/dogecoin/dogecoin/blob/master/doc/fee-recommendation.md 13 | the min fee rate is now 0.01 DOGE/kB which is 0.00001 DOGE/byte or 14 | 1,000 sats/vByte . 15 | */ 16 | const minRelayFeeRate = 1000 17 | 18 | return { 19 | forceUseUtxo: (args: UtxoPickerArgs): UtxoPickerResult => { 20 | if (args.feeRate < minRelayFeeRate) { 21 | args.feeRate = minRelayFeeRate 22 | } 23 | return forceUseUtxo(args) 24 | }, 25 | subtractFee: (args: UtxoPickerArgs): UtxoPickerResult => { 26 | if (args.feeRate < minRelayFeeRate) { 27 | args.feeRate = minRelayFeeRate 28 | } 29 | return subtractFee(args) 30 | }, 31 | accumulative: (args: UtxoPickerArgs): UtxoPickerResult => { 32 | if (args.feeRate < minRelayFeeRate) { 33 | args.feeRate = minRelayFeeRate 34 | } 35 | return accumulative(args) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/common/utxobased/keymanager/bip69.ts: -------------------------------------------------------------------------------- 1 | /*** 2 | * https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki 3 | */ 4 | 5 | import { Input, Output } from './utxopicker' 6 | 7 | /*** 8 | * Previous transaction hashes (in reversed byte-order) are to be sorted in ascending order, lexicographically. 9 | * In the event of two matching transaction hashes, 10 | * the respective previous output indices will be compared by their integer value, in ascending order. 11 | * If the previous output indices match, the inputs are considered equal. 12 | */ 13 | export const sortInputs = (inputs: Input[]): Input[] => 14 | [...inputs].sort((a, b) => { 15 | const aHash = Buffer.from(a.hash).reverse() 16 | const bHash = Buffer.from(b.hash).reverse() 17 | const compare = aHash.compare(bHash) 18 | return compare !== 0 ? compare : a.index - b.index 19 | }) 20 | 21 | /*** 22 | * Transaction output amounts (as 64-bit unsigned integers) are to be sorted in ascending order. 23 | * In the event of two matching output amounts, 24 | * the respective output scriptPubKeys (as a byte-array) will be compared lexicographically, in ascending order. 25 | * If the scriptPubKeys match, the outputs are considered equal. 26 | * If the previous output indices match, the inputs are considered equal. 27 | */ 28 | export const sortOutputs = (outputs: Output[]): Output[] => 29 | [...outputs].sort((a, b) => 30 | a.value === b.value ? a.script.compare(b.script) : a.value - b.value 31 | ) 32 | -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/scriptPubkeyByPath/bip49_0/0.json: -------------------------------------------------------------------------------- 1 | ["a9142cee2a1dc30c73232c945eb39e389cec172178ed87","a91438d5c9084ddc8e7c6abb34751fb24ce84738c39d87","a9143e5c2d1ac6394bed008d5723a74d7a49c6d7b6e287","a9142244ce86d664e85801f7eb2a56dd35afd268212587","a914af3366b24f9b542742b7f99e1adeab4faa7d58a087","a914a1bbc3898f44d673e445c44db978811e3bd3bfe287","a9142c93bdc7b9ff33aac4574a66087e8a5c9962ab6a87","a914cb3cbcd22f416bf9cb703b14f28ef2ba155c4da887","a914a74131941b19acb351e93d582be11bd21905168d87","a914b01a433772f0b32e45397ab3335ac33edb8b6b4387","a914989dadf417d6c1e25878f5552153f146932f40ee87","a914da9622ec02c0899861f3c4c3b860411260e84a2787","a914bb1d6b2628fca0b13a0983f35161eaa1951e386987","a9143d823b2f44104cae843c1355b25bcfe8bd4c317d87","a914d7fa9a636b3ec37e4264144dacb66f546357b0c887","a914ef46f6fd8c9aad8456576deffecf2755c6acb39d87","a914b2506341c9ff8111ab8b1e81fabf7fba3569879f87","a914d808f1819fe39b497b40f5298e92110e8ef063e987","a9145c7cb6419a5be938c5a087288256d4dcd1c89d5687","a91457095572458c84a4d77fef2f9c373d483958c9d387","a91428b6b5eab84ae05bccf801a1ea72a0266525ea7887","a91453b998215518395c781cf4500fbdcc8c90ea352187","a914921078cc11de8f6ac1563350d61901efbaa709fa87","a914649768982cc96d1d210dfe6ecf38517680d6b6dd87","a914d23bb3234ccbc4f9873b203c8b5d32bcadcfa4af87","a914a2296268c9aa1af6fbb5857da753e0d99a7c4e1587","a9148393c44c64136faf7a53945cf3157ec7b180d60087","a914c218708e294309e4e1b887ef3af9883f879553d187","a9144452b33291fd22a2274758eebd14650a35ba1f1587"] -------------------------------------------------------------------------------- /src/common/fees/processMempoolSpaceFees.ts: -------------------------------------------------------------------------------- 1 | import * as bs from 'biggystring' 2 | import { asNumber, asObject } from 'cleaners' 3 | 4 | import { LOW_FEE } from '../constants' 5 | import { FeeRates } from '../plugin/types' 6 | 7 | export const asMempoolSpaceFees = asObject({ 8 | fastestFee: asNumber, 9 | halfHourFee: asNumber, 10 | hourFee: asNumber 11 | }) 12 | 13 | /** 14 | * Calculates the FeeRates object from MempoolSpace 15 | * Sets the minimum of LOW_FEE on all fee levels 16 | * @param fees 17 | * @returns Partial 18 | */ 19 | 20 | const STANDARD_FEE_HIGH_MULTIPLIER = 1.3 21 | const HIGH_FEE_MULTIPLIER = 2 22 | 23 | export const processMempoolSpaceFees = (fees: unknown): FeeRates | null => { 24 | let mempoolFees: ReturnType 25 | try { 26 | mempoolFees = asMempoolSpaceFees(fees) 27 | } catch { 28 | return null 29 | } 30 | 31 | const { fastestFee, halfHourFee } = mempoolFees 32 | 33 | const lowFee = halfHourFee.toString() 34 | const standardFeeLow = fastestFee.toString() 35 | const standardFeeHigh = Math.round( 36 | fastestFee * STANDARD_FEE_HIGH_MULTIPLIER 37 | ).toString() 38 | const highFee = Math.round(fastestFee * HIGH_FEE_MULTIPLIER).toString() 39 | 40 | return { 41 | lowFee: bs.max(lowFee, LOW_FEE.toString()), 42 | standardFeeLow: bs.max(standardFeeLow, LOW_FEE.toString()), 43 | standardFeeHigh: bs.max(standardFeeHigh, LOW_FEE.toString()), 44 | highFee: bs.max(highFee, LOW_FEE.toString()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/common/utxobased/network/windowWS.ts: -------------------------------------------------------------------------------- 1 | import { InnerSocket, InnerSocketCallbacks, ReadyState } from './types' 2 | 3 | export function setupBrowser( 4 | uri: string, 5 | callbacks: InnerSocketCallbacks 6 | ): InnerSocket { 7 | if (window.WebSocket == null) 8 | throw Error('Native browser WebSocket does not exists') 9 | 10 | const socket = new window.WebSocket(uri) 11 | socket.onopen = () => { 12 | callbacks.onOpen() 13 | } 14 | socket.onmessage = (message: MessageEvent) => { 15 | callbacks.onMessage(message.data.toString()) 16 | } 17 | socket.onerror = event => { 18 | callbacks.onError(event) 19 | } 20 | socket.onclose = event => { 21 | callbacks.onClose(event.code, event.reason) 22 | } 23 | 24 | return { 25 | get readyState(): ReadyState { 26 | switch (socket.readyState) { 27 | case WebSocket.CONNECTING: 28 | return ReadyState.CONNECTING 29 | case WebSocket.OPEN: 30 | return ReadyState.OPEN 31 | case WebSocket.CLOSING: 32 | return ReadyState.CLOSING 33 | case WebSocket.CLOSED: 34 | default: 35 | return ReadyState.CLOSED 36 | } 37 | }, 38 | 39 | disconnect() { 40 | if ( 41 | socket == null || 42 | socket.readyState === WebSocket.CLOSING || 43 | socket.readyState === WebSocket.CLOSED 44 | ) 45 | return 46 | 47 | socket.close() 48 | }, 49 | 50 | send(data: string) { 51 | socket.send(data) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/common/upgradeMemos.ts: -------------------------------------------------------------------------------- 1 | import { EdgeCurrencyInfo, EdgeMemo, EdgeSpendInfo } from 'edge-core-js/types' 2 | 3 | import { validateMemos } from './validateMemos' 4 | 5 | /** 6 | * Upgrades the memo fields inside an EdgeSpendTarget, 7 | * since we need to be runtime-compatible with legacy core versions. 8 | */ 9 | export function upgradeMemos( 10 | spendInfo: EdgeSpendInfo, 11 | currencyInfo: EdgeCurrencyInfo 12 | ): EdgeSpendInfo { 13 | const { memoType } = currencyInfo 14 | 15 | const legacyMemos: EdgeMemo[] = [] 16 | 17 | // If this chain supports legacy memos, grab those: 18 | if (memoType === 'hex' || memoType === 'number' || memoType === 'text') { 19 | for (const target of spendInfo.spendTargets) { 20 | if (target.memo != null) { 21 | legacyMemos.push({ 22 | type: memoType, 23 | value: target.memo 24 | }) 25 | } else if (target.uniqueIdentifier != null) { 26 | legacyMemos.push({ 27 | type: memoType, 28 | value: target.uniqueIdentifier 29 | }) 30 | } else if (typeof target.otherParams?.uniqueIdentifier === 'string') { 31 | legacyMemos.push({ 32 | type: memoType, 33 | value: target.otherParams.uniqueIdentifier 34 | }) 35 | } 36 | } 37 | } 38 | 39 | // If we don't have modern memos, use the legacy ones: 40 | const out: EdgeSpendInfo = { 41 | ...spendInfo, 42 | memos: spendInfo.memos ?? legacyMemos 43 | } 44 | 45 | validateMemos(out, currencyInfo) 46 | return out 47 | } 48 | -------------------------------------------------------------------------------- /src/common/utxobased/network/nodejsWS.ts: -------------------------------------------------------------------------------- 1 | import WS from 'ws' 2 | 3 | import { InnerSocket, InnerSocketCallbacks, ReadyState } from './types' 4 | 5 | export function setupWS( 6 | uri: string, 7 | callbacks: InnerSocketCallbacks 8 | ): InnerSocket { 9 | const ws = new WS(uri, { 10 | headers: { 11 | 'User-Agent': 'NodeJS-WS-agent' 12 | } 13 | }) 14 | 15 | ws.on('open', () => { 16 | callbacks.onOpen() 17 | }) 18 | ws.on('message', (data: WS.Data) => { 19 | // eslint-disable-next-line @typescript-eslint/no-base-to-string 20 | callbacks.onMessage(data.toString()) 21 | }) 22 | ws.on('error', error => { 23 | callbacks.onError(error) 24 | }) 25 | ws.on('close', (code: number, reason: string) => { 26 | callbacks.onClose(code, reason) 27 | }) 28 | 29 | return { 30 | get readyState(): ReadyState { 31 | switch (ws.readyState) { 32 | case WS.CONNECTING: 33 | return ReadyState.CONNECTING 34 | case WS.OPEN: 35 | return ReadyState.OPEN 36 | case WS.CLOSING: 37 | return ReadyState.CLOSING 38 | case WS.CLOSED: 39 | default: 40 | return ReadyState.CLOSED 41 | } 42 | }, 43 | 44 | disconnect(): void { 45 | if ( 46 | ws == null || 47 | ws.readyState === WS.CLOSING || 48 | ws.readyState === WS.CLOSED 49 | ) 50 | return 51 | ws.removeAllListeners() 52 | ws.close() 53 | }, 54 | 55 | send(data: string): void { 56 | ws.send(data) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/common/utxobased/network/socketQueue.ts: -------------------------------------------------------------------------------- 1 | const QUEUE_JOBS_PER_RUN = 3 2 | const QUEUE_RUN_DELAY = 1000 3 | 4 | interface UpdateQueue { 5 | id: string 6 | action?: string 7 | updateFunc: () => void 8 | } 9 | 10 | const updateQueue: UpdateQueue[] = [] 11 | let timeOut: ReturnType 12 | 13 | export function pushUpdate(update: UpdateQueue): void { 14 | const shouldRun = updateQueue.length === 0 15 | let didUpdate = false 16 | for (const u of updateQueue) { 17 | if (u.id === update.id && u.action === update.action) { 18 | u.updateFunc = update.updateFunc 19 | didUpdate = true 20 | break 21 | } 22 | } 23 | if (!didUpdate) { 24 | updateQueue.push(update) 25 | } 26 | if (shouldRun) { 27 | startQueue() 28 | } 29 | } 30 | 31 | export function removeIdFromQueue(id: string): void { 32 | for (let i = 0; i < updateQueue.length; i++) { 33 | const update = updateQueue[i] 34 | if (id === update.id) { 35 | updateQueue.splice(i, 1) 36 | break 37 | } 38 | } 39 | if (updateQueue.length === 0) { 40 | clearTimeout(timeOut) 41 | } 42 | } 43 | 44 | function startQueue(): void { 45 | const numJobs = 46 | QUEUE_JOBS_PER_RUN < updateQueue.length 47 | ? QUEUE_JOBS_PER_RUN 48 | : updateQueue.length 49 | for (let i = 0; i < numJobs; i++) { 50 | if (updateQueue.length > 0) { 51 | const u = updateQueue.shift() 52 | u?.updateFunc() 53 | } 54 | } 55 | if (updateQueue.length > 0) { 56 | timeOut = setTimeout(() => { 57 | startQueue() 58 | }, QUEUE_RUN_DELAY) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/common/utxobased/keymanager/utxopicker/accumulative.ts: -------------------------------------------------------------------------------- 1 | import { Output, UTXO, UtxoPickerArgs, UtxoPickerResult } from './types' 2 | import * as utils from './utils' 3 | // add inputs until we reach or surpass the target value (or deplete) 4 | // worst-case: O(n) 5 | 6 | export function accumulative(args: UtxoPickerArgs): UtxoPickerResult { 7 | const { utxos, targets, feeRate, changeScript } = args 8 | 9 | if (!isFinite(utils.uintOrNaN(feeRate))) { 10 | throw new Error('No rate provided') 11 | } 12 | 13 | const outputs: Output[] = targets.map(target => ({ 14 | ...target, 15 | script: Buffer.from(target.script, 'hex'), 16 | scriptPubkey: Buffer.from(target.script, 'hex') 17 | })) 18 | 19 | let inValue = 0 20 | const inputs: UTXO[] = [] 21 | const targetValue = utils.sumOrNaN(targets) 22 | 23 | for (let i = 0; i < utxos.length; ++i) { 24 | const utxo = utxos[i] 25 | 26 | // skip detrimental input 27 | const utxoFee = feeRate * utils.inputBytes(utxo) 28 | if (utxoFee > utxo.value) { 29 | if (i === utxos.length - 1) { 30 | break 31 | } else { 32 | continue 33 | } 34 | } 35 | 36 | inputs.push(utxo) 37 | inValue += utxo.value 38 | 39 | const bytes = utils.transactionBytes(inputs, outputs) 40 | const fee = bytes * feeRate 41 | 42 | // go again? 43 | if (inValue < targetValue + fee) continue 44 | 45 | return utils.finalize(inputs, outputs, feeRate, changeScript) 46 | } 47 | 48 | return { 49 | changeUsed: false, 50 | fee: Math.ceil(feeRate * utils.transactionBytes(inputs, outputs)), 51 | inputs 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/common/plugin/Metadata.spec.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai' 2 | import chaiAsPromised from 'chai-as-promised' 3 | import { Disklet, makeMemoryDisklet } from 'disklet' 4 | import { MemoryStorage } from 'disklet/lib/src/backends/memory' 5 | import { EdgeLog } from 'edge-core-js/types' 6 | 7 | import { 8 | EngineEmitter, 9 | EngineEvent 10 | } from '../../../src/common/plugin/EngineEmitter' 11 | import { makeMetadata, Metadata } from '../../../src/common/plugin/Metadata' 12 | import { makeFakeLog } from '../../utils' 13 | 14 | chai.should() 15 | chai.use(chaiAsPromised) 16 | 17 | const wait = async (seconds: number): Promise => 18 | await new Promise(resolve => setTimeout(resolve, seconds * 1000)) 19 | 20 | describe('makeMetadata', () => { 21 | const memory: MemoryStorage = {} 22 | let disklet: Disklet 23 | let metadata: Metadata 24 | const log: EdgeLog = makeFakeLog() 25 | const emitter = new EngineEmitter() 26 | 27 | before(async () => { 28 | disklet = makeMemoryDisklet(memory) 29 | metadata = await makeMetadata({ 30 | disklet, 31 | emitter, 32 | log 33 | }) 34 | }) 35 | 36 | describe('block height update', () => { 37 | it('should only ever increase the block height', async function () { 38 | this.timeout(3000) 39 | 40 | metadata.state.lastSeenBlockHeight.should.eql(0) 41 | 42 | emitter.emit(EngineEvent.BLOCK_HEIGHT_CHANGED, '', 10) 43 | await wait(1) 44 | metadata.state.lastSeenBlockHeight.should.eql(10) 45 | 46 | emitter.emit(EngineEvent.BLOCK_HEIGHT_CHANGED, '', 5) 47 | await wait(1) 48 | metadata.state.lastSeenBlockHeight.should.eql(10) 49 | }) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /test/common/plugin/CurrencyPlugin.fixtures/common.ts: -------------------------------------------------------------------------------- 1 | import { EdgeEncodeUri, EdgeWalletInfo } from 'edge-core-js/types' 2 | 3 | import { 4 | EncodeUriMetadata, 5 | ExtendedParseUri 6 | } from '../../../../src/common/plugin/types' 7 | 8 | export interface FixtureType { 9 | pluginId: string 10 | WALLET_TYPE: string 11 | WALLET_FORMAT: string 12 | 'Test Currency code': string 13 | key: number[] 14 | xpub: string 15 | 'invalid key name': EdgeWalletInfo 16 | 'invalid wallet type': EdgeWalletInfo 17 | importKey: { 18 | validKeys: string[] 19 | invalidKeys: string[] 20 | unsupportedKeys: string[] 21 | } 22 | parseUri: { 23 | [testName: string]: [string, ExtendedParseUri] | [string] 24 | } 25 | encodeUri: { 26 | [testName: string]: 27 | | [EdgeEncodeUri & EncodeUriMetadata, string] 28 | | [EdgeEncodeUri & EncodeUriMetadata] 29 | } 30 | getSplittableTypes?: { 31 | [walletFormat: string]: string[] 32 | } 33 | } 34 | 35 | // The mnemonic and key represent the same private key 36 | const mnemonic = 37 | 'chicken valve parrot park animal proof youth detail glance review artwork cluster drive more charge lunar uncle neglect brain act rose job photo spot' 38 | export const key = [ 39 | 39, 40 | 190, 41 | 34, 42 | 129, 43 | 208, 44 | 32, 45 | 145, 46 | 88, 47 | 191, 48 | 217, 49 | 226, 50 | 98, 51 | 183, 52 | 16, 53 | 52, 54 | 150, 55 | 52, 56 | 53, 57 | 31, 58 | 137, 59 | 164, 60 | 40, 61 | 236, 62 | 146, 63 | 128, 64 | 107, 65 | 129, 66 | 59, 67 | 192, 68 | 240, 69 | 40, 70 | 238 71 | ] 72 | export const mnemonics = [mnemonic] 73 | export const airbitzSeeds = ['AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='] 74 | -------------------------------------------------------------------------------- /test/common/utxobased/engine/engine.fixtures/bitcoinTestnet/dummyData/tables/scriptPubkeyByPath/bip49_1/0.json: -------------------------------------------------------------------------------- 1 | ["a9141cf9c7ff6ef78b78e47765922f824f0153e987d787","a9141f74859dcf06afa47e15c8ece5277b60bbc5b04387","a914e04d6d45c7437509f2b6cedeb6315078a0b4608487","a914bf76ff07ad44824990e72be8d85aa102b8cedc1287","a9144a4b87abba761724f7a5a8c0fba78d9544c012e787","a9147cffcf53d21c7b405e3089bacfbf830d3c012c6887","a9141d646ffdcd9ab40c5d6a31710b0c1cbdda2aeda287","a91430bd80f8b0beb1c30738d57090ce188746a298db87","a914bbeecf1b89b5bb7adfcc02feca57416c14b2b24f87","a9147a38669dfc5147ca79d4fea0b2b78c3838ce4d1787","a9141c48c5335c2e78d99051b797d6d43dabbbf41a6c87","a9141380c031262e59b42a284e87da11a4802eaf8e1987","a914347defe6f22eaeef9f309f47a3aaad74373e4bab87","a914ae7a57b4a6568360f5126d278e0302f0207fdf0987","a914de23511e8b5f37f6e9f4a2d0fe0c294fdbd5c67387","a914b9862104c52909a56811e18a10c7d3122223822887","a9148f70336fdc8416664422443bece3dda7ff6eecf987","a9144d4cbdad294022e5226a51d8db42c426e5a1b81c87","a914c4566ce9f721884d727d51762373af79d611875a87","a914ae39a8e6ab12259293912b90f4c411a3e507d55787","a91419b947c203be2b81fbf2df5a839a04a5a3d4e4c787","a914a857a42d3310ca1df556f3c66fdfa4b43d22dd2a87","a9142b6a43856d28276b4ece62dc215875d32efb701687","a91445bb77fc8d98d5b9d443542f808f36d6f183f3d487","a9142c81d63debd886083fa857191714af7c57e5443787","a914359e463b19e95b37a0f4e77e8dc32fa303084ab987","a914c0066116b189508480021f788b6f75c0bb06fe6087","a9149e5875c02a692b28ef04bd95b993ae39533200af87","a9149a3a1373ea803daea029b0ada97208313a1157a487","a9144a1a8338554264c4bd7cec75764a2e3a9c8d307687","a914d1be0b70ed4b76551f29791f5eaccc9fb4231e4187","a914763ce446a95abfb457eecd8f8f9078c80c9253ae87","a914008ac628bbbbf70237f0f45eb7c95f66a064f9cd87","a9141f0d1a2245d94ebdb201ef83873b5dcaa29d744587","a914b99bd84ffd5d4d48fcaa4d24566bd9a825463f8487"] -------------------------------------------------------------------------------- /src/common/utxobased/engine/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | asArray, 3 | asBoolean, 4 | asMaybe, 5 | asNumber, 6 | asObject, 7 | asOptional, 8 | asString, 9 | asValue 10 | } from 'cleaners' 11 | import { EdgeSpendInfo } from 'edge-core-js/types' 12 | 13 | import { asTxOptions } from '../../plugin/types' 14 | import { Input, Output } from '../keymanager/utxopicker/types' 15 | 16 | export interface UtxoInitOptions { 17 | nowNodesApiKey?: string 18 | } 19 | export const asUtxoInitOptions = asObject({ 20 | nowNodesApiKey: asOptional(asString) 21 | }) 22 | 23 | export const asUtxoUserSettings = asObject({ 24 | blockbookServers: asMaybe(asArray(asString), []), 25 | enableCustomServers: asMaybe(asBoolean, false) 26 | }) 27 | export type UtxoUserSettings = ReturnType 28 | 29 | export interface UtxoTxOtherParams { 30 | unsignedTx: string // hex 31 | psbt?: { 32 | base64: string 33 | inputs: Input[] 34 | outputs: Output[] 35 | } 36 | edgeSpendInfo?: EdgeSpendInfo 37 | ourScriptPubkeys: string[] 38 | replacedTxid?: string 39 | } 40 | 41 | export type UtxoSignMessageOtherParams = ReturnType< 42 | typeof asUtxoSignMessageOtherParams 43 | > 44 | export const asUtxoSignMessageOtherParams = asObject({ 45 | publicAddress: asString 46 | }) 47 | 48 | const asOutputSort = asValue('bip69', 'targets') 49 | 50 | export type UtxoSpendInfoOtherParams = ReturnType< 51 | typeof asUtxoSpendInfoOtherParams 52 | > 53 | export const asUtxoSpendInfoOtherParams = asObject({ 54 | /** @deprecated use `EdgeSpendInfo['enableRbf']` */ 55 | enableRbf: asOptional(asBoolean), 56 | forceChangeAddress: asOptional(asString), 57 | memoIndex: asOptional(asNumber), 58 | outputSort: asOptional(asOutputSort, 'bip69'), 59 | txOptions: asOptional(asTxOptions), 60 | utxoSourceAddress: asOptional(asString) 61 | }) 62 | -------------------------------------------------------------------------------- /src/common/utxobased/keymanager/base.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import bs58 from 'bs58' 4 | import { Buffer } from 'buffer' 5 | 6 | interface Base58BaseReturn { 7 | encode: (payload: Buffer) => string 8 | decode: (stringPayload: string | undefined) => Buffer 9 | decodeUnsafe: (stringPayload: string) => Buffer | undefined 10 | } 11 | 12 | export function base58Base( 13 | checksumFn: (buffer: Buffer) => Buffer 14 | ): Base58BaseReturn { 15 | // Encode a buffer as a base58-check encoded string 16 | function encode(payload: Buffer): string { 17 | const checksum: Buffer = checksumFn(payload) 18 | 19 | return bs58.encode(Buffer.concat([payload, checksum], payload.length + 4)) 20 | } 21 | 22 | function decodeRaw(buffer: Buffer): Buffer | undefined { 23 | const payload = buffer.slice(0, -4) 24 | const checksum = buffer.slice(-4) 25 | const newChecksum = checksumFn(payload) 26 | 27 | if ( 28 | ((checksum[0] ^ newChecksum[0]) | 29 | (checksum[1] ^ newChecksum[1]) | 30 | (checksum[2] ^ newChecksum[2]) | 31 | (checksum[3] ^ newChecksum[3])) === 32 | 1 33 | ) 34 | return 35 | 36 | return payload 37 | } 38 | 39 | // Decode a base58-check encoded string to a buffer, no result if checksum is wrong 40 | function decodeUnsafe(stringPayload: string): Buffer | undefined { 41 | const buffer = bs58.decodeUnsafe(stringPayload) 42 | if (buffer == null) return 43 | 44 | return decodeRaw(buffer) 45 | } 46 | 47 | function decode(payload: string | undefined): Buffer { 48 | if (payload == null) throw new Error('Invalid base58 string') 49 | const buffer = bs58.decode(payload) 50 | const bPayload = decodeRaw(buffer) 51 | if (bPayload == null) throw new Error('Invalid checksum') 52 | return bPayload 53 | } 54 | 55 | return { 56 | encode, 57 | decode, 58 | decodeUnsafe 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/common/fees/fees.spec.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai' 2 | import chaiAsPromised from 'chai-as-promised' 3 | import { makeMemoryDisklet } from 'disklet' 4 | import { MemoryStorage } from 'disklet/lib/src/backends/memory' 5 | 6 | import { FEES_PATH } from '../../../src/common/constants' 7 | import { Fees, makeFees } from '../../../src/common/fees/makeFees' 8 | import { FeeInfo } from '../../../src/common/plugin/types' 9 | import { makeFakeIo, makeFakeLog, makeFakePluginInfo } from '../../utils' 10 | 11 | chai.should() 12 | chai.use(chaiAsPromised) 13 | const { expect } = chai 14 | 15 | describe('fees', function () { 16 | const fakeIo = makeFakeIo() 17 | const fakeLog = makeFakeLog() 18 | const fakeMakePluginInfo = makeFakePluginInfo() 19 | const memory: MemoryStorage = {} 20 | const disklet = makeMemoryDisklet(memory) 21 | let fees: Fees 22 | 23 | const testJson = (expected?: FeeInfo): void => { 24 | const str = memory[`/${FEES_PATH}`] 25 | if (typeof str === 'string') { 26 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions 27 | expected == null 28 | ? expect(str).to.be.undefined 29 | : expect(JSON.parse(str)).to.eql(expected) 30 | } 31 | } 32 | 33 | describe('makeFees', () => { 34 | it('should load fees from the currency info file on wallet first load', async () => { 35 | fees = await makeFees({ 36 | disklet, 37 | pluginInfo: fakeMakePluginInfo, 38 | io: fakeIo, 39 | log: fakeLog 40 | }) 41 | 42 | fees.feeInfo.should.eql(fakeMakePluginInfo.engineInfo.defaultFeeInfo) 43 | 44 | testJson() 45 | }) 46 | 47 | it('should cache fees after started', async () => { 48 | await fees.start() 49 | 50 | testJson(fakeMakePluginInfo.engineInfo.defaultFeeInfo) 51 | 52 | // be sure to stop after start, test will hang otherwise 53 | fees.stop() 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /src/common/utxobased/engine/await-lock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A mutex lock for coordination across async functions 3 | */ 4 | export default class AwaitLock { 5 | private _acquired = false 6 | private readonly _waitingResolvers: Array<() => void> = [] 7 | 8 | /** 9 | * Whether the lock is currently acquired or not. Accessing this property does not affect the 10 | * status of the lock. 11 | */ 12 | get acquired(): boolean { 13 | return this._acquired 14 | } 15 | 16 | /** 17 | * Acquires the lock, waiting if necessary for it to become free if it is already locked. The 18 | * returned promise is fulfilled once the lock is acquired. 19 | * 20 | * After acquiring the lock, you **must** call `release` when you are done with it. 21 | */ 22 | async acquireAsync(): Promise { 23 | if (!this._acquired) { 24 | this._acquired = true 25 | return await Promise.resolve() 26 | } 27 | 28 | return await new Promise(resolve => { 29 | this._waitingResolvers.push(resolve) 30 | }) 31 | } 32 | 33 | /** 34 | * Acquires the lock if it is free and otherwise returns immediately without waiting. Returns 35 | * `true` if the lock was free and is now acquired, and `false` otherwise, 36 | */ 37 | tryAcquire(): boolean { 38 | if (!this._acquired) { 39 | this._acquired = true 40 | return true 41 | } 42 | 43 | return false 44 | } 45 | 46 | /** 47 | * Releases the lock and gives it to the next waiting acquirer, if there is one. Each acquirer 48 | * must release the lock exactly once. 49 | */ 50 | release(): void { 51 | if (!this._acquired) { 52 | throw new Error(`Cannot release an unacquired lock`) 53 | } 54 | 55 | if (this._waitingResolvers.length > 0) { 56 | const resolve = this._waitingResolvers.shift() 57 | if (resolve == null) { 58 | throw new Error('Cannot release null lock') 59 | } 60 | resolve() 61 | } else { 62 | this._acquired = false 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/common/utxobased/keymanager/utxopicker/forceUseUtxo.ts: -------------------------------------------------------------------------------- 1 | import { Output, UTXO, UtxoPickerArgs, UtxoPickerResult } from './types' 2 | import * as utils from './utils' 3 | // implementation very similar to accumulative strategy 4 | // add inputs until we reach or surpass the target value (or deplete) 5 | // worst-case: O(n) 6 | 7 | export function forceUseUtxo(args: UtxoPickerArgs): UtxoPickerResult { 8 | const { utxos, useUtxos, targets, feeRate, changeScript } = args 9 | 10 | if (!isFinite(utils.uintOrNaN(feeRate))) { 11 | throw new Error('No rate provided') 12 | } 13 | 14 | const outputs: Output[] = targets.map(target => ({ 15 | ...target, 16 | script: Buffer.from(target.script, 'hex'), 17 | scriptPubkey: Buffer.from(target.script, 'hex') 18 | })) 19 | 20 | const inputs: UTXO[] = useUtxos ?? [] 21 | let inValue = inputs.reduce((n, { value }) => n + value, 0) 22 | const targetValue = utils.sumOrNaN(targets) 23 | const bytes = utils.transactionBytes(inputs, outputs) 24 | const fee = bytes * feeRate 25 | // if the new feeRate is already covered by lowering the change amount, return 26 | if (inValue >= targetValue + fee) { 27 | return utils.finalize(inputs, outputs, feeRate, changeScript) 28 | } 29 | 30 | for (let i = 0; i < utxos.length; ++i) { 31 | const utxo = utxos[i] 32 | 33 | // skip detrimental input 34 | const utxoFee = feeRate * utils.inputBytes(utxo) 35 | if (utxoFee > utxo.value) { 36 | if (i === utxos.length - 1) { 37 | break 38 | } else { 39 | continue 40 | } 41 | } 42 | 43 | inputs.push(utxo) 44 | inValue += utxo.value 45 | 46 | const bytes = utils.transactionBytes(inputs, outputs) 47 | const fee = bytes * feeRate 48 | 49 | // go again? 50 | if (inValue < targetValue + fee) continue 51 | 52 | return utils.finalize(inputs, outputs, feeRate, changeScript) 53 | } 54 | 55 | return { 56 | changeUsed: false, 57 | fee: Math.ceil(feeRate * utils.transactionBytes(inputs, outputs)), 58 | inputs 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/common/utxobased/info/scriptTemplates/bitcoincashScriptTemplates.ts: -------------------------------------------------------------------------------- 1 | import { OptionalScriptTemplateArgs, ScriptTemplates } from './types' 2 | 3 | const OP_CHECKDATASIGVERIFY = 'bb' 4 | const OP_CHECKDATASIG = 'ba' 5 | const OP_CHECKSIG = 'ac' 6 | const CDS_SIGNATURE = 7 | '30440220256c12175e809381f97637933ed6ab97737d263eaaebca6add21bced67fd12a402205ce29ecc1369d6fc1b51977ed38faaf41119e3be1d7edfafd7cfaf0b6061bd07' 8 | const CDS_MESSAGE = '' 9 | const CDS_PUBKEY = 10 | '038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508' 11 | 12 | const hexToVarByte = (hex: string): string => { 13 | const len = hex.length / 2 14 | const str = len.toString(16) 15 | const hexLen = str.length % 2 === 0 ? str : `0${str}` 16 | return hexLen + hex 17 | } 18 | 19 | const cds = ( 20 | cdsSigs: string, 21 | cdsMsg: string, 22 | cdsPubKey: string, 23 | pubKey: string 24 | ): string[] => { 25 | const cdsSuffix = `${hexToVarByte(pubKey)}${OP_CHECKSIG}` 26 | const cdsPrefix = `${hexToVarByte(cdsSigs)}${hexToVarByte( 27 | cdsMsg 28 | )}${hexToVarByte(cdsPubKey)}` 29 | return [cdsPrefix, cdsSuffix] 30 | } 31 | 32 | export const scriptTemplates: ScriptTemplates = { 33 | replayProtection: (pubKey: string): string => { 34 | return cds(CDS_SIGNATURE, CDS_MESSAGE, CDS_PUBKEY, pubKey).join( 35 | OP_CHECKDATASIGVERIFY 36 | ) 37 | }, 38 | checkdatasig: ( 39 | pubKey: string, 40 | optionalArgs?: OptionalScriptTemplateArgs 41 | ): string => { 42 | const cdsSig = optionalArgs?.cdsSig ?? '' 43 | const cdsMsg = optionalArgs?.cdsMsg ?? '' 44 | const cdsPubKey = optionalArgs?.cdsPubKey ?? '' 45 | return cds(cdsSig, cdsMsg, cdsPubKey, pubKey).join(OP_CHECKDATASIG) 46 | }, 47 | checkdatasigverify: ( 48 | pubKey: string, 49 | optionalArgs?: OptionalScriptTemplateArgs 50 | ): string => { 51 | const cdsSig = optionalArgs?.cdsSig ?? '' 52 | const cdsMsg = optionalArgs?.cdsMsg ?? '' 53 | const cdsPubKey = optionalArgs?.cdsPubKey ?? '' 54 | return cds(cdsSig, cdsMsg, cdsPubKey, pubKey).join(OP_CHECKDATASIGVERIFY) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/common/utxobased/keymanager/bitcoincashUtils/base32.ts: -------------------------------------------------------------------------------- 1 | /*** 2 | * https://github.com/bitcoincashjs/cashaddr 3 | * Copyright (c) 2018 Matias Alejo Garcia 4 | * Copyright (c) 2017 Emilio Almansi 5 | * Distributed under the MIT software license, see the accompanying 6 | * file LICENSE or http://www.opensource.org/licenses/mit-license.php. 7 | */ 8 | 9 | /*** 10 | * Charset containing the 32 symbols used in the base32 encoding. 11 | */ 12 | const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l' 13 | 14 | /*** 15 | * Inverted index mapping each symbol into its index within the charset. 16 | */ 17 | 18 | interface CHARSET_INVERSE_INDEX_TYPE { 19 | [value: string]: number 20 | } 21 | 22 | const CHARSET_INVERSE_INDEX: CHARSET_INVERSE_INDEX_TYPE = { 23 | q: 0, 24 | p: 1, 25 | z: 2, 26 | r: 3, 27 | y: 4, 28 | 9: 5, 29 | x: 6, 30 | 8: 7, 31 | g: 8, 32 | f: 9, 33 | 2: 10, 34 | t: 11, 35 | v: 12, 36 | d: 13, 37 | w: 14, 38 | 0: 15, 39 | s: 16, 40 | 3: 17, 41 | j: 18, 42 | n: 19, 43 | 5: 20, 44 | 4: 21, 45 | k: 22, 46 | h: 23, 47 | c: 24, 48 | e: 25, 49 | 6: 26, 50 | m: 27, 51 | u: 28, 52 | a: 29, 53 | 7: 30, 54 | l: 31 55 | } 56 | 57 | /*** 58 | * Encodes the given array of 5-bit integers as a base32-encoded string. 59 | * 60 | * @param {Array} data Array of integers between 0 and 31 inclusive. 61 | */ 62 | export function encode(data: number[]): string { 63 | let base32 = '' 64 | for (let i = 0; i < data.length; i++) { 65 | const value = data[i] 66 | if (value < 0 || value > 32) throw new Error(`InvalidArgument: ${value}`) 67 | base32 += CHARSET[value] 68 | } 69 | return base32 70 | } 71 | 72 | /*** 73 | * Decodes the given base32-encoded string into an array of 5-bit integers. 74 | * 75 | * @param {string} base32 76 | */ 77 | export function decode(base32: string): number[] { 78 | const data = [] 79 | for (let i = 0; i < base32.length; i++) { 80 | const value = base32[i] 81 | if (!(value in CHARSET_INVERSE_INDEX)) { 82 | throw new Error(`InvalidArgument: ${value} not in CHARSET_INVERSE_INDEX`) 83 | } 84 | data.push(CHARSET_INVERSE_INDEX[value]) 85 | } 86 | return data 87 | } 88 | -------------------------------------------------------------------------------- /src/common/utxobased/db/types.ts: -------------------------------------------------------------------------------- 1 | import { asBoolean, asNumber, asObject, asOptional, asString } from 'cleaners' 2 | 3 | import { SoftPick } from '../../../util/typeUtil' 4 | import { AddressPath } from '../../plugin/types' 5 | import { asScriptTypeEnum, ScriptTypeEnum } from '../keymanager/keymanager' 6 | 7 | export interface AddressData { 8 | scriptPubkey: string 9 | redeemScript?: string 10 | lastQueriedBlockHeight: number 11 | path?: AddressPath 12 | lastQuery: number 13 | lastTouched: number 14 | used: boolean 15 | balance?: string 16 | } 17 | 18 | export const makeAddressData = ( 19 | addressFields: SoftPick 20 | ): AddressData => { 21 | const { scriptPubkey, used = false, ...rest } = addressFields 22 | 23 | return { 24 | scriptPubkey, 25 | used, 26 | lastQueriedBlockHeight: 0, 27 | lastQuery: 0, 28 | lastTouched: 0, 29 | ...rest 30 | } 31 | } 32 | 33 | export interface UtxoData { 34 | id: string 35 | txid: string 36 | vout: number 37 | value: string 38 | scriptPubkey: string 39 | script: string 40 | redeemScript?: string 41 | scriptType: ScriptTypeEnum 42 | blockHeight: number 43 | spent: boolean 44 | } 45 | export const asUtxoData = asObject({ 46 | id: asString, 47 | txid: asString, 48 | vout: asNumber, 49 | value: asString, 50 | scriptPubkey: asString, 51 | script: asString, 52 | redeemScript: asOptional(asString), 53 | scriptType: asScriptTypeEnum, 54 | blockHeight: asNumber, 55 | spent: asBoolean 56 | }) 57 | 58 | export interface TransactionData { 59 | txid: string 60 | hex: string 61 | blockHeight: number 62 | confirmations?: 'confirmed' | 'unconfirmed' | 'dropped' | number 63 | date: number 64 | fees: string 65 | inputs: TransactionDataInput[] 66 | outputs: TransactionDataOutput[] 67 | ourIns: string[] 68 | ourOuts: string[] 69 | ourAmount: string 70 | } 71 | 72 | export interface TransactionDataOutput { 73 | amount: string 74 | n: number 75 | scriptPubkey: string 76 | } 77 | 78 | export interface TransactionDataInput { 79 | amount: string 80 | n: number 81 | outputIndex: number 82 | scriptPubkey: string 83 | sequence: number 84 | txId: string 85 | } 86 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent any 3 | tools { 4 | nodejs "stable" 5 | } 6 | options { 7 | timestamps() 8 | skipDefaultCheckout true 9 | overrideIndexTriggers false 10 | buildDiscarder logRotator(artifactDaysToKeepStr: '', artifactNumToKeepStr: '', daysToKeepStr: '7', numToKeepStr: '10') 11 | disableConcurrentBuilds() 12 | } 13 | triggers { 14 | pollSCM("H/5 * * * *") 15 | } 16 | 17 | stages { 18 | stage("Clean the workspace and checkout source") { 19 | steps { 20 | deleteDir() 21 | checkout scm 22 | } 23 | } 24 | 25 | stage ("Install Dependencies") { 26 | steps { 27 | sh "yarn" 28 | } 29 | } 30 | 31 | stage ("Check Lint") { 32 | steps { 33 | sh "yarn lint" 34 | } 35 | } 36 | 37 | stage ("Check Types") { 38 | steps { 39 | sh "yarn types" 40 | } 41 | } 42 | 43 | stage ("Test Package") { 44 | steps { 45 | sh "yarn coverage" 46 | } 47 | } 48 | } 49 | 50 | post { 51 | always { 52 | echo 'Setting the build version' 53 | script { 54 | def packageJson = readJSON file: "./package.json" 55 | currentBuild.description = "[version] ${packageJson.version}" 56 | } 57 | echo 'Trying to publish the test report' 58 | junit healthScaleFactor: 100.0, testResults: '**/coverage/junit.xml', allowEmptyResults: true 59 | echo 'Trying to publish the code coverage report' 60 | cobertura( 61 | coberturaReportFile: '**/coverage/cobertura-coverage.xml', 62 | failUnhealthy: false, 63 | failNoReports: false, 64 | failUnstable: false, 65 | onlyStable: false, 66 | zoomCoverageChart: false, 67 | conditionalCoverageTargets: '70, 0, 0', 68 | lineCoverageTargets: '70, 0, 0', 69 | methodCoverageTargets: '70, 0, 0', 70 | maxNumberOfBuilds: 0, 71 | sourceEncoding: 'ASCII' 72 | ) 73 | } 74 | success { 75 | echo "The force is strong with this one" 76 | deleteDir() 77 | } 78 | unstable { 79 | echo "Do or do not there is no try" 80 | } 81 | failure { 82 | echo "The dark side I sense in you." 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/common/utxobased/db/util/utxo.ts: -------------------------------------------------------------------------------- 1 | import { currencyFormatToPurposeType } from '../../engine/utils' 2 | import { 3 | BIP43PurposeTypeEnum, 4 | ScriptTypeEnum 5 | } from '../../keymanager/keymanager' 6 | import { DataLayer } from '../DataLayer' 7 | import { TransactionData, UtxoData } from '../types' 8 | 9 | export const utxoFromTransactionDataInput = async ( 10 | dataLayer: DataLayer, 11 | transactionData: TransactionData, 12 | inputIndex: number 13 | ): Promise => { 14 | const input = transactionData.inputs[inputIndex] 15 | const address = await dataLayer.fetchAddress(input.scriptPubkey) 16 | 17 | if (address == null) 18 | throw new Error(`Cannot find address for ${input.scriptPubkey}`) 19 | if (address.path == null) 20 | throw new Error(`Address has no derivation path information`) 21 | 22 | // Optional redeemScript for "our addresses" 23 | const redeemScript = address.redeemScript 24 | 25 | const purposeType = currencyFormatToPurposeType(address.path.format) 26 | const getScripts = async (): Promise<{ 27 | script: string 28 | scriptType: ScriptTypeEnum 29 | }> => { 30 | switch (purposeType) { 31 | case BIP43PurposeTypeEnum.Airbitz: 32 | case BIP43PurposeTypeEnum.Legacy: { 33 | return { 34 | script: transactionData.hex, 35 | scriptType: 36 | redeemScript != null ? ScriptTypeEnum.p2sh : ScriptTypeEnum.p2pkh 37 | } 38 | } 39 | case BIP43PurposeTypeEnum.WrappedSegwit: 40 | return { 41 | script: input.scriptPubkey, 42 | scriptType: ScriptTypeEnum.p2wpkhp2sh 43 | } 44 | case BIP43PurposeTypeEnum.Segwit: 45 | return { 46 | script: input.scriptPubkey, 47 | scriptType: ScriptTypeEnum.p2wpkh 48 | } 49 | default: 50 | throw new Error(`Unknown purpose type ${purposeType}`) 51 | } 52 | } 53 | const { script, scriptType } = await getScripts() 54 | 55 | return { 56 | id: `${input.txId}_${input.outputIndex}`, 57 | txid: input.txId, 58 | vout: input.outputIndex, 59 | value: input.amount, 60 | scriptPubkey: input.scriptPubkey, 61 | script, 62 | redeemScript, 63 | scriptType, 64 | blockHeight: transactionData.blockHeight, 65 | spent: true 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/common/utxobased/info/badcoin.ts: -------------------------------------------------------------------------------- 1 | import { EdgeCurrencyInfo } from 'edge-core-js/types' 2 | 3 | import { CoinInfo, EngineInfo, PluginInfo } from '../../plugin/types' 4 | import { 5 | legacyMemoInfo, 6 | utxoCustomFeeTemplate, 7 | utxoMemoOptions 8 | } from './commonInfo' 9 | 10 | const currencyInfo: EdgeCurrencyInfo = { 11 | assetDisplayName: 'Badcoin', 12 | chainDisplayName: 'Badcoin', 13 | currencyCode: 'BAD', 14 | customFeeTemplate: utxoCustomFeeTemplate, 15 | memoOptions: utxoMemoOptions, 16 | pluginId: 'badcoin', 17 | walletType: 'wallet:badcoin', 18 | 19 | // Explorers: 20 | addressExplorer: 'https://www.blockingbad.com/address/%s', 21 | blockExplorer: 'https://www.blockingbad.com/block/%s', 22 | transactionExplorer: 'https://www.blockingbad.com/tx/%s', 23 | 24 | denominations: [ 25 | { name: 'BAD', multiplier: '100000000', symbol: 'BAD' }, 26 | { name: 'mBAD', multiplier: '100000', symbol: 'mBAD' } 27 | ], 28 | 29 | // Deprecated: 30 | ...legacyMemoInfo, 31 | defaultSettings: { 32 | customFeeSettings: ['satPerByte'], 33 | blockbookServers: [], 34 | enableCustomServers: false 35 | }, 36 | displayName: 'Badcoin', 37 | metaTokens: [] 38 | } 39 | 40 | const engineInfo: EngineInfo = { 41 | formats: ['bip49', 'bip44', 'bip32'], 42 | gapLimit: 10, 43 | feeUpdateInterval: 60000, 44 | defaultFeeInfo: { 45 | lowFeeFudgeFactor: undefined, 46 | standardFeeLowFudgeFactor: undefined, 47 | standardFeeHighFudgeFactor: undefined, 48 | highFeeFudgeFactor: undefined, 49 | 50 | highFee: '300', 51 | lowFee: '100', 52 | standardFeeLow: '150', 53 | standardFeeHigh: '200', 54 | standardFeeLowAmount: '20000000', 55 | standardFeeHighAmount: '981000000' 56 | } 57 | } 58 | 59 | export const coinInfo: CoinInfo = { 60 | name: 'badcoin', 61 | segwit: false, 62 | coinType: 324, 63 | prefixes: { 64 | messagePrefix: [ 65 | '\x18Badcoin Signed Message:\n', 66 | '\x18Bitcoin Signed Message:\n' 67 | ], 68 | wif: [0xb0, 0x80], 69 | legacyXPriv: [0x06c4abc9, 0x0488ade4], 70 | legacyXPub: [0x06c4abc8, 0x0488b21e], 71 | pubkeyHash: [0x1c, 0x00], 72 | scriptHash: [0x19, 0x05] 73 | } 74 | } 75 | 76 | export const info: PluginInfo = { 77 | currencyInfo, 78 | engineInfo, 79 | coinInfo 80 | } 81 | -------------------------------------------------------------------------------- /patches/altcoin-js+1.0.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/altcoin-js/src/transaction.js b/node_modules/altcoin-js/src/transaction.js 2 | index ec6af8c..a137e15 100644 3 | --- a/node_modules/altcoin-js/src/transaction.js 4 | +++ b/node_modules/altcoin-js/src/transaction.js 5 | @@ -53,6 +53,7 @@ class Transaction { 6 | const flag = bufferReader.readUInt8(); 7 | let hasWitnesses = false; 8 | if ( 9 | + tx.version !== 3 && // avoid segwit for PIVX 10 | marker === Transaction.ADVANCED_TRANSACTION_MARKER && 11 | flag === Transaction.ADVANCED_TRANSACTION_FLAG 12 | ) { 13 | @@ -88,7 +89,14 @@ class Transaction { 14 | tx.locktime = bufferReader.readUInt32(); 15 | if (_NO_STRICT) return tx; 16 | if (bufferReader.offset !== buffer.length) 17 | - throw new Error('Transaction has unexpected data'); 18 | + { 19 | + // Throw for all transaction other then PIVX transactions 20 | + if (tx.version !== 3) { 21 | + throw new Error('Transaction has unexpected data'); 22 | + } 23 | + // Keep the sapling data for PIVX transactions 24 | + tx.saplingData = bufferReader.readSlice(bufferReader.buffer.length - bufferReader.offset); 25 | + } 26 | return tx; 27 | } 28 | static fromHex(hex) { 29 | @@ -155,6 +163,7 @@ class Transaction { 30 | } 31 | byteLength(_ALLOW_WITNESS = true) { 32 | const hasWitnesses = _ALLOW_WITNESS && this.hasWitnesses(); 33 | + const hasSaplingData = this.saplingData != null; 34 | return ( 35 | (hasWitnesses ? 10 : 8) + 36 | bufferutils_1.varuint.encodingLength(this.ins.length) + 37 | @@ -169,7 +178,8 @@ class Transaction { 38 | ? this.ins.reduce((sum, input) => { 39 | return sum + vectorSize(input.witness); 40 | }, 0) 41 | - : 0) 42 | + : 0) + 43 | + (hasSaplingData ? this.saplingData.length : 0) 44 | ); 45 | } 46 | clone() { 47 | @@ -604,6 +614,10 @@ class Transaction { 48 | }); 49 | } 50 | bufferWriter.writeUInt32(this.locktime); 51 | + // Read sapling data for PIVX transactions 52 | + if (this.saplingData) { 53 | + bufferWriter.writeSlice(this.saplingData); 54 | + } 55 | // avoid slicing unless necessary 56 | if (initialOffset !== undefined) 57 | return buffer.slice(initialOffset, bufferWriter.offset); 58 | -------------------------------------------------------------------------------- /src/common/utxobased/info/ravencoin.ts: -------------------------------------------------------------------------------- 1 | import { EdgeCurrencyInfo } from 'edge-core-js/types' 2 | 3 | import { CoinInfo, EngineInfo, PluginInfo } from '../../plugin/types' 4 | import { maximumFeeRateCalculator } from '../../plugin/util/maximumFeeRateCalculator' 5 | import { 6 | legacyMemoInfo, 7 | utxoCustomFeeTemplate, 8 | utxoMemoOptions 9 | } from './commonInfo' 10 | 11 | const currencyInfo: EdgeCurrencyInfo = { 12 | assetDisplayName: 'Ravencoin', 13 | chainDisplayName: 'Ravencoin', 14 | currencyCode: 'RVN', 15 | customFeeTemplate: utxoCustomFeeTemplate, 16 | memoOptions: utxoMemoOptions, 17 | pluginId: 'ravencoin', 18 | walletType: 'wallet:ravencoin', 19 | 20 | // Explorers: 21 | addressExplorer: 'https://rvn.cryptoscope.io/address/?address=%s', 22 | blockExplorer: 'https://rvn.cryptoscope.io/block/?blockheight=%s', 23 | transactionExplorer: 'https://rvn.cryptoscope.io/tx/?txid=%s', 24 | 25 | denominations: [{ name: 'RVN', multiplier: '100000000', symbol: 'R' }], 26 | 27 | // Deprecated: 28 | ...legacyMemoInfo, 29 | defaultSettings: { 30 | customFeeSettings: ['satPerByte'], 31 | blockbookServers: ['wss://blockbook.ravencoin.org'], 32 | enableCustomServers: false 33 | }, 34 | displayName: 'Ravencoin', 35 | metaTokens: [] 36 | } 37 | 38 | const engineInfo: EngineInfo = { 39 | formats: ['bip44', 'bip32'], 40 | gapLimit: 10, 41 | feeUpdateInterval: 60000, 42 | defaultFeeInfo: { 43 | lowFeeFudgeFactor: undefined, 44 | standardFeeLowFudgeFactor: undefined, 45 | standardFeeHighFudgeFactor: undefined, 46 | highFeeFudgeFactor: undefined, 47 | 48 | highFee: '150', 49 | lowFee: '20', 50 | standardFeeLow: '50', 51 | standardFeeHigh: '100', 52 | standardFeeLowAmount: '173200', 53 | standardFeeHighAmount: '8670000', 54 | maximumFeeRate: maximumFeeRateCalculator(currencyInfo, 0.01419) 55 | } 56 | } 57 | 58 | export const coinInfo: CoinInfo = { 59 | name: 'ravencoin', 60 | segwit: false, 61 | coinType: 175, 62 | prefixes: { 63 | messagePrefix: ['\x18Bitcoin Signed Message:\n'], 64 | wif: [0x80], 65 | legacyXPriv: [0x0488ade4], 66 | legacyXPub: [0x0488b21e], 67 | pubkeyHash: [0x3c], 68 | scriptHash: [0x7a] 69 | } 70 | } 71 | 72 | export const info: PluginInfo = { 73 | currencyInfo, 74 | engineInfo, 75 | coinInfo 76 | } 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Edge Currency Plugin for UTXO-Based currencies 2 | 3 | > Plugins for [edge-core-js](https://github.com/EdgeApp/edge-core-js), to support various cryptocurrencies such as bitcoin, bitcoin cash, and litecoin, 4 | 5 | ## Installing 6 | 7 | First, add this library to your project: 8 | 9 | ```sh 10 | npm i -s edge-currency-plugins 11 | ``` 12 | 13 | ### Node.js 14 | 15 | For Node.js, you should call `addEdgeCorePlugins` to register these plugins with edge-core-js: 16 | 17 | ```js 18 | const { addEdgeCorePlugins, lockEdgeCorePlugins } = require("edge-core-js"); 19 | const plugins = require("edge-currency-plugins"); 20 | 21 | addEdgeCorePlugins(plugins); 22 | 23 | // Once you are done adding plugins, call this: 24 | lockEdgeCorePlugins(); 25 | ``` 26 | 27 | You can also add plugins individually if you want to be more picky: 28 | 29 | ```js 30 | addEdgeCorePlugins({ 31 | bitcoin: plugins.bitcoin, 32 | }); 33 | ``` 34 | 35 | ### Browser 36 | 37 | The bundle located in `dist/edge-currency-plugins.js` will automatically register itself with edge-core-js. Just serve the entire `dist` directory along with your app, and then load the script: 38 | 39 | ```html 40 |