├── .gitignore ├── .mocharc.yml ├── .nycrc ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── README.md ├── examples ├── 1-get-token-balances-local.ts ├── 1-get-token-balances-trusted.ts ├── 10-burn-token.ts ├── 11-get-txns-from-xpub-grpc.ts ├── 2-get-txn-details-local.ts ├── 2-get-txn-details-trusted.ts ├── 3-genesis-token-type-1.ts ├── 4-genesis-token-type-NFT1-parent.ts ├── 5-genesis-token-type-NFT1-child.ts ├── 6-mint-token.ts ├── 7-send-token-p2pkh.ts ├── 8-send-token-p2sh-frozen.ts ├── 9-send-token-p2sh-multisig.ts └── 9-send-token-p2sh-p2pkh.ts ├── index.ts ├── lib ├── bchdnetwork.ts ├── bchdtrustedvalidator.ts ├── bitboxnetwork.ts ├── bitdbnetwork.ts ├── crypto.ts ├── localvalidator.ts ├── primatives.ts ├── script.ts ├── slp.ts ├── slptokentype1.ts ├── transactionhelpers.ts ├── trustedvalidator.ts ├── utils.ts └── vendors.d.ts ├── package-lock.json ├── package.json ├── test ├── bchdnetwork.test.ts ├── bitboxnetwork.test.ts ├── bitdbnetwork.test.ts ├── crypto.test.ts ├── global.d.ts ├── localvalidator.test.ts ├── slp.test.ts ├── slptokentype1.test.ts └── utils.test.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | yarn.lock 4 | 5 | # dependencies 6 | /node_modules 7 | 8 | # ignore built js files 9 | *.js 10 | index.js 11 | *.js.map 12 | *.d.ts 13 | 14 | # testing 15 | /coverage 16 | 17 | # production 18 | /dist 19 | 20 | # misc 21 | .DS_Store 22 | .env.local 23 | .env.development.local 24 | .env.test.local 25 | .env.production.local 26 | 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | .nyc_output 32 | -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - 'ts-node/register' 3 | - 'source-map-support/register' 4 | recursive: true 5 | spec: 'test/**/*.test.ts' 6 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@istanbuljs/nyc-config-typescript", 3 | "all": false 4 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | - 14.15 5 | sudo: false 6 | script: 7 | - npm test 8 | after_success: 9 | - npm run coverage -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Current TS File", 9 | "type": "node", 10 | "request": "launch", 11 | "args": ["${relativeFile}"], 12 | "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], 13 | "sourceMaps": true, 14 | "cwd": "${workspaceRoot}", 15 | "protocol": "inspector", 16 | }, 17 | { 18 | "type": "node", 19 | "request": "launch", 20 | "name": "Run Mocha", 21 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 22 | "args": ["${workspaceRoot}/test/**/*.test.ts"], 23 | "cwd": "${workspaceRoot}", 24 | "outFiles": [] 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.json": "jsonc" 4 | } 5 | } -------------------------------------------------------------------------------- /examples/1-get-token-balances-local.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * 3 | * Example 1: Fetch token balances for an address 4 | * using BCHD client and Local Validator 5 | * 6 | * NOTE: See example file: "1a-get-token-balances-bchd-trusted.ts" for trusted validator 7 | * using SLPDB. 8 | * 9 | * Instructions: 10 | * (1) - Select Network and Address by commenting/un-commenting the desired 11 | * TESTNET or MAINNET section and providing valid BCH address. 12 | * (2) - Run `ts-node ` just before script execution, 13 | * or use vscode debugger w/ launch.json settings for "Current TS File" 14 | * 15 | * ************************************************************************************/ 16 | 17 | import * as BITBOXSDK from "bitbox-sdk"; 18 | const BITBOX = new BITBOXSDK.BITBOX(); 19 | import { GrpcClient } from "grpc-bchrpc-node"; 20 | import { LocalValidator, SlpBalancesResult } from "../index"; 21 | import { BchdNetwork } from "../lib/bchdnetwork"; 22 | import { GetRawTransactionsAsync } from "../lib/localvalidator"; 23 | 24 | // MAINNET NETWORK 25 | const addr = "simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu"; 26 | const testnet = false; 27 | 28 | // TESTNET NETWORK 29 | // const addr = "slptest:qrzp09cnyysvsjc0s63kflmdmewuuwvs4gc8h7uh86"; 30 | // const testnet = true; 31 | 32 | // NOTE: you will want to override the "url" parameter with a local node in production use, 33 | const client = new GrpcClient({testnet}); 34 | 35 | // VALIDATOR: FOR LOCAL VALIDATOR 36 | const getRawTransactions: GetRawTransactionsAsync = async (txids: string[]) => { 37 | const getRawTransaction = async (txid: string) => { 38 | console.log(`Downloading: ${txid}`); 39 | return await client.getRawTransaction({hash: txid, reversedHashOrder: true}); 40 | }; 41 | return (await Promise.all( 42 | txids.map((txid) => getRawTransaction(txid)))) 43 | .map((res) => Buffer.from(res.getTransaction_asU8()).toString("hex")); 44 | }; 45 | 46 | const logger = console; 47 | const validator = new LocalValidator(BITBOX, getRawTransactions, logger); 48 | const network = new BchdNetwork({BITBOX, client, validator}); 49 | 50 | (async () => { 51 | console.log(`Checking balances for ${addr}`); 52 | const balances = (await network.getAllSlpBalancesAndUtxos(addr)) as SlpBalancesResult; 53 | let counter = 0; 54 | for (const key in balances.slpTokenBalances) { 55 | counter++; 56 | const tokenInfo = await network.getTokenInformation(key); 57 | console.log(`TokenId: ${key}, SLP Type: ${tokenInfo.versionType}, Balance: ${balances.slpTokenBalances[key].div(10 ** tokenInfo.decimals).toFixed(tokenInfo.decimals)}`); 58 | } 59 | for (const key in balances.nftParentChildBalances) { 60 | counter++; 61 | // TODO ... 62 | } 63 | if (counter === 0) { 64 | console.log("No tokens found for this address."); 65 | } 66 | })(); 67 | -------------------------------------------------------------------------------- /examples/1-get-token-balances-trusted.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * 3 | * Example 1a: Fetch token balances for an address 4 | * using BCHD and SLPDB Trusted Validation 5 | * 6 | * Instructions: 7 | * (1) - Select Network and Address by commenting/un-commenting the desired 8 | * TESTNET or MAINNET section and providing valid BCH address. 9 | * (2) - Run `ts-node ` just before script execution, 10 | * or use vscode debugger w/ launch.json settings for "Current TS File" 11 | * 12 | * ************************************************************************************/ 13 | 14 | import * as BITBOXSDK from "bitbox-sdk"; 15 | const BITBOX = new BITBOXSDK.BITBOX(); 16 | import { GrpcClient } from "grpc-bchrpc-node"; 17 | import { BchdNetwork, BchdValidator, SlpBalancesResult } from "../index"; 18 | 19 | const addr = "simpleledger:qpcgsyu3c4hd00luwhc5a9x5zcgnlw8kdqmdxyjsta"; 20 | 21 | const client = new GrpcClient({ url: "bchd.ny1.simpleledger.io" }); 22 | const validator = new BchdValidator(client); 23 | const network = new BchdNetwork({BITBOX, client, validator}); 24 | 25 | (async () => { 26 | console.log(`Checking balances for ${addr}`); 27 | const balances = (await network.getAllSlpBalancesAndUtxos(addr)) as SlpBalancesResult; 28 | let counter = 0; 29 | for (const key in balances.slpTokenBalances) { 30 | counter++; 31 | const tokenInfo = await network.getTokenInformation(key); 32 | console.log(`TokenId: ${key}, SLP Type: ${tokenInfo.versionType}, Balance: ${balances.slpTokenBalances[key].div(10 ** tokenInfo.decimals).toFixed(tokenInfo.decimals)}`); 33 | } 34 | for (const key in balances.nftParentChildBalances) { 35 | counter++; 36 | // TODO ... 37 | } 38 | if (counter === 0) { 39 | console.log("No tokens found for this address."); 40 | } 41 | })(); 42 | -------------------------------------------------------------------------------- /examples/10-burn-token.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * 3 | * Example 10: Burn any type of token. 4 | * 5 | * Instructions: 6 | * (1) - Select Network and Address by commenting/uncommenting the desired 7 | * NETWORK section and providing valid BCH address. 8 | * 9 | * (2) - Select a Validation method by commenting/uncommenting the desired 10 | * VALIDATOR section. Chose from remote validator or local validator. 11 | * Both options rely on remote JSON RPC calls to rest.bitcoin.com. 12 | * - Option 1: REMOTE VALIDATION (rest.bitcoin.com/v2/slp/isTxidValid/) 13 | * - Option 2: LOCAL VALIDATOR / REST JSON RPC 14 | * - Option 3: LOCAL VALIDATOR / LOCAL FULL NODE 15 | * 16 | * (3) - Run `tsc && node ` just before script execution, or for 17 | * debugger just run `tsc` in the console and then use vscode debugger 18 | * with "Launch Current File" mode. 19 | * 20 | * ************************************************************************************/ 21 | 22 | import { BigNumber } from "bignumber.js"; 23 | import * as BITBOXSDK from "bitbox-sdk"; 24 | import { BitboxNetwork, LocalValidator, SlpBalancesResult } from "../index"; 25 | 26 | (async () => { 27 | 28 | // NETWORK: FOR MAINNET 29 | const BITBOX = new BITBOXSDK.BITBOX({ restURL: "https://rest.bitcoin.com/v2/" }); 30 | const fundingAddress = "simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu"; // <-- must be simpleledger format 31 | const fundingWif = "L3gngkDg1HW5P9v5GdWWiCi3DWwvw5XnzjSPwNwVPN5DSck3AaiF"; // <-- compressed WIF format 32 | const bchChangeReceiverAddress = "simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu"; // <-- must be simpleledger format 33 | const tokenId = "d32b4191d3f78909f43a3f5853ba59e9f2d137925f28e7780e717f4b4bfd4a3f"; 34 | const burnAmount = 1; 35 | 36 | // VALIDATOR: Option 1: FOR REMOTE VALIDATION 37 | // const bitboxNetwork = new BitboxNetwork(BITBOX); 38 | 39 | // VALIDATOR: Option 2: FOR LOCAL VALIDATOR / REMOTE JSON RPC 40 | // const getRawTransactions: GetRawTransactionsAsync = async function(txids: string[]) { 41 | // return await BITBOX.RawTransactions.getRawTransaction(txids) 42 | // } 43 | // const logger = console; 44 | // const slpValidator = new LocalValidator(BITBOX, getRawTransactions, logger); 45 | // const bitboxNetwork = new BitboxNetwork(BITBOX, slpValidator); 46 | 47 | // VALIDATOR: Option 3: LOCAL VALIDATOR / LOCAL FULL NODE JSON RPC 48 | const logger = console; 49 | const RpcClient = require("bitcoin-rpc-promise"); 50 | const connectionString = "http://bitcoin:password@localhost:8332" 51 | const rpc = new RpcClient(connectionString); 52 | const slpValidator = new LocalValidator(BITBOX, async (txids) => [ await rpc.getRawTransaction(txids[0]) ], logger) 53 | const bitboxNetwork = new BitboxNetwork(BITBOX, slpValidator); 54 | 55 | // 1) Fetch critical token information 56 | const tokenInfo = await bitboxNetwork.getTokenInformation(tokenId); 57 | const tokenDecimals = tokenInfo.decimals; 58 | console.log("Token precision:", tokenDecimals.toString()); 59 | 60 | // 2) Check that token balance is greater than our desired sendAmount 61 | const balances = await bitboxNetwork.getAllSlpBalancesAndUtxos(fundingAddress); 62 | if (balances.slpTokenBalances[tokenId]) { 63 | console.log("Token balance:", balances.slpTokenBalances[tokenId].toFixed() / 10**tokenDecimals) 64 | } else { 65 | console.log("Funding addresss is not holding any tokens with id:", tokenId); 66 | } 67 | 68 | // 3) Calculate send amount in "Token Satoshis". In this example we want to just send 1 token unit to someone... 69 | const amount = (new BigNumber(burnAmount)).times(10**tokenDecimals); // Don't forget to account for token precision 70 | 71 | // 4) Get all of our token's UTXOs 72 | let inputUtxos = balances.slpTokenUtxos[tokenId]; 73 | 74 | // 5) Simply sweep our BCH utxos to fuel the transaction 75 | inputUtxos = inputUtxos.concat(balances.nonSlpUtxos); 76 | 77 | // 6) Set the proper private key for each Utxo 78 | inputUtxos.forEach((txo) => txo.wif = fundingWif); 79 | 80 | // 7) Send token 81 | const sendTxid = await bitboxNetwork.simpleTokenBurn( 82 | tokenId, 83 | amount, 84 | inputUtxos, 85 | bchChangeReceiverAddress 86 | ); 87 | 88 | console.log("BURN txn complete:", sendTxid); 89 | })(); 90 | -------------------------------------------------------------------------------- /examples/11-get-txns-from-xpub-grpc.ts: -------------------------------------------------------------------------------- 1 | import * as bitcore from "bitcore-lib-cash"; 2 | import { GrpcClient, Transaction } from "grpc-bchrpc-node"; 3 | import { Utils } from "../lib/utils"; 4 | 5 | const gprc = new GrpcClient(); 6 | 7 | // user variables 8 | const tokenId = ""; 9 | const address = ""; 10 | 11 | const xpub = "xpub661MyMwAqRbcEmunind5AZXnevFW66TB3vq5MHM5Asq8UNaEdTsgk4njwUXW4RGywGK68au91R1rvjQ6SmJQEUwUinjYZPnJA7o72bG5HFr"; 12 | // @ts-ignore 13 | const hdPublickey = new bitcore.HDPublicKey(xpub); 14 | const accountDerivation = "m/0/"; // this is the account part of the non-hardened HD path so, "//
/" 15 | let lastFoundActiveAddressIndex = 0; 16 | const addressGapScanDepth = 10; // this is the gap that will be maintained past the "lastActiveAddressIndex" 17 | let transactionHistory: Transaction[] = []; 18 | 19 | // main scanning loop 20 | (async () => { 21 | for (let i = 0; i < lastFoundActiveAddressIndex + addressGapScanDepth; i++) { 22 | // get address 23 | const orderPublickey = hdPublickey.deriveChild(accountDerivation + i.toFixed()); 24 | // @ts-ignore 25 | const pubkey = new bitcore.PublicKey(orderPublickey.publicKey); 26 | // @ts-ignore 27 | const address = bitcore.Address.fromPublicKey(pubkey, bitcore.Networks.mainnet).toString(); 28 | console.log(address); 29 | //const cashAddr = Utils.toCashAddress(address); 30 | //console.log(cashAddr); 31 | const res = await gprc.getAddressTransactions({address}); 32 | if (res.getConfirmedTransactionsList().length > 0) { 33 | lastFoundActiveAddressIndex = i; 34 | transactionHistory.push(...res.getConfirmedTransactionsList()); 35 | console.log("Has transactions!"); 36 | } 37 | console.log(transactionHistory.length); 38 | } 39 | })(); 40 | -------------------------------------------------------------------------------- /examples/2-get-txn-details-local.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * 3 | * Example 2: Fetch token details for given txid 4 | * 5 | * Instructions: 6 | * (1) - Select Network and Address by commenting/un-commenting the desired 7 | * NETWORK section and providing valid BCH address. 8 | * (2) - Select a Validation method by commenting/un-commenting the desired 9 | * VALIDATOR section. Chose from remote validator or local validator. 10 | * Both options rely on remote JSON RPC calls to rest.bitcoin.com. 11 | * (3) - Run `tsc && node ` just before script execution 12 | * (4) - Optional: Use vscode debugger w/ launch.json settings 13 | * 14 | * ************************************************************************************/ 15 | 16 | import * as BITBOXSDK from "bitbox-sdk"; 17 | import { GrpcClient } from "grpc-bchrpc-node"; 18 | import { BchdNetwork, LocalValidator } from "../index"; 19 | import { GetRawTransactionsAsync } from "../lib/localvalidator"; 20 | 21 | const BITBOX = new BITBOXSDK.BITBOX(); 22 | const logger = console; 23 | 24 | // NETWORK: FOR TESTNET COMMENT/UNCOMMENT 25 | const txid = "0c681b2df346ffde3d856de12216a974a123ff593c410244b650b67a7ea606c4"; 26 | const testnet = true; 27 | 28 | // NETWORK: FOR MAINNET COMMENT/UNCOMMENT 29 | // let txid = ""; 30 | // const testnet = false; 31 | 32 | // VALIDATOR: FOR LOCAL VALIDATOR 33 | const client = new GrpcClient({testnet}); 34 | 35 | // VALIDATOR: FOR LOCAL VALIDATOR 36 | const getRawTransactions: GetRawTransactionsAsync = async (txids: string[]) => { 37 | const getRawTransaction = async (txid: string) => { 38 | console.log(`Downloading: ${txid}`); 39 | return await client.getRawTransaction({hash: txid, reversedHashOrder: true}); 40 | }; 41 | return (await Promise.all( 42 | txids.map((txid) => getRawTransaction(txid)))) 43 | .map((res) => Buffer.from(res.getTransaction_asU8()).toString("hex")); 44 | }; 45 | const validator = new LocalValidator(BITBOX, getRawTransactions, logger); 46 | const bitboxNetwork = new BchdNetwork({ BITBOX, client, validator }); 47 | 48 | (async () => { 49 | const details = await bitboxNetwork.getTransactionDetails(txid); 50 | console.log("Transaction SLP details: ", details); 51 | })(); 52 | -------------------------------------------------------------------------------- /examples/2-get-txn-details-trusted.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * 3 | * Example 2: Fetch token details for given txid 4 | * 5 | * Instructions: 6 | * (1) - Select Network and Address by commenting/un-commenting the desired 7 | * NETWORK section and providing valid BCH address. 8 | * (2) - Select a Validation method by commenting/un-commenting the desired 9 | * VALIDATOR section. Chose from remote validator or local validator. 10 | * Both options rely on remote JSON RPC calls to rest.bitcoin.com. 11 | * (3) - Run `tsc && node ` just before script execution 12 | * (4) - Optional: Use vscode debugger w/ launch.json settings 13 | * 14 | * ************************************************************************************/ 15 | 16 | import * as BITBOXSDK from "bitbox-sdk"; 17 | import { GrpcClient } from "grpc-bchrpc-node"; 18 | import { BchdNetwork } from "../index"; 19 | import { GetRawTransactionsAsync } from "../lib/localvalidator"; 20 | import { TrustedValidator } from "../lib/trustedvalidator"; 21 | 22 | const BITBOX = new BITBOXSDK.BITBOX(); 23 | const logger = console; 24 | 25 | // NETWORK: FOR MAINNET COMMENT/UNCOMMENT 26 | const txid = "c67c6423767a86e27c56ad9c04581f4500d88baff12b865611a39602f449b465"; 27 | const testnet = false; 28 | 29 | // VALIDATOR: FOR LOCAL VALIDATOR 30 | const client = new GrpcClient({testnet}); 31 | 32 | // VALIDATOR: FOR LOCAL VALIDATOR 33 | const getRawTransactions: GetRawTransactionsAsync = async (txids: string[]) => { 34 | const getRawTransaction = async (txid: string) => { 35 | console.log(`Downloading: ${txid}`); 36 | return await client.getRawTransaction({hash: txid, reversedHashOrder: true}); 37 | }; 38 | return (await Promise.all( 39 | txids.map((txid) => getRawTransaction(txid)))) 40 | .map((res) => Buffer.from(res.getTransaction_asU8()).toString("hex")); 41 | }; 42 | 43 | // NOTE: for testnet you would need to set a testnet slpdbUrl 44 | const validator = new TrustedValidator({ getRawTransactions, logger }); 45 | const network = new BchdNetwork({ BITBOX, client, validator }); 46 | 47 | (async () => { 48 | const details = await network.getTransactionDetails(txid); 49 | console.log("Transaction SLP details: ", details); 50 | })(); 51 | -------------------------------------------------------------------------------- /examples/3-genesis-token-type-1.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * 3 | * Example 3: Genesis for SLP Token Type 1 4 | * 5 | * Instructions: 6 | * (1) - Send some BCH to simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu 7 | * or tBCH to slptest:qpwyc9jnwckntlpuslg7ncmhe2n423304ueqcyw80l 8 | * to fund the example. 9 | * (2) - Select Network and Address by commenting/un-commenting the desired 10 | * NETWORK section and providing valid BCH address. 11 | * (3) - Select a Validation method by commenting/un-commenting the desired 12 | * VALIDATOR section. Chose from remote validator or local validator. 13 | * Both options rely on remote JSON RPC calls to rest.bitcoin.com. 14 | * (4) - Run `tsc && node ` just before script execution 15 | * (5) - Optional: Use vscode debugger w/ launch.json settings 16 | * 17 | * ************************************************************************************/ 18 | 19 | import { BigNumber } from "bignumber.js"; 20 | import * as BITBOXSDK from "bitbox-sdk"; 21 | import { BitboxNetwork, SlpBalancesResult } from "../index"; 22 | 23 | const decimals = 2; 24 | const name = "Awesome SLPJS README Token"; 25 | const ticker = "SLPJS"; 26 | const documentUri = "info@simpleledger.io"; 27 | const documentHash: Buffer|null = null; 28 | const initialTokenQty = 1000000; 29 | 30 | (async () => { 31 | 32 | // NETWORK: FOR MAINNET UNCOMMENT 33 | const BITBOX = new BITBOXSDK.BITBOX({ restURL: "https://rest.bitcoin.com/v2/" }); 34 | const fundingAddress = "simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu"; // <-- must be simpleledger format 35 | const fundingWif = "L3gngkDg1HW5P9v5GdWWiCi3DWwvw5XnzjSPwNwVPN5DSck3AaiF"; // <-- compressed WIF format 36 | const tokenReceiverAddress = "simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu"; // <-- must be simpleledger format 37 | const bchChangeReceiverAddress = "simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu"; // <-- cashAddr or slpAddr format 38 | // For unlimited issuance provide a "batonReceiverAddress" 39 | const batonReceiverAddress = "simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu"; 40 | 41 | // NETWORK: FOR TESTNET UNCOMMENT 42 | // const BITBOX = new BITBOXSDK.BITBOX({ restURL: 'https://trest.bitcoin.com/v2/' }); 43 | // const fundingAddress = "slptest:qpwyc9jnwckntlpuslg7ncmhe2n423304ueqcyw80l"; 44 | // const fundingWif = "cVjzvdHGfQDtBEq7oddDRcpzpYuvNtPbWdi8tKQLcZae65G4zGgy"; 45 | // const tokenReceiverAddress = "slptest:qpwyc9jnwckntlpuslg7ncmhe2n423304ueqcyw80l"; 46 | // const bchChangeReceiverAddress = "slptest:qpwyc9jnwckntlpuslg7ncmhe2n423304ueqcyw80l"; 47 | // // For unlimited issuance provide a "batonReceiverAddress" 48 | // const batonReceiverAddress = "slptest:qpwyc9jnwckntlpuslg7ncmhe2n423304ueqcyw80l"; 49 | 50 | const bitboxNetwork = new BitboxNetwork(BITBOX); 51 | 52 | // 1) Get all balances at the funding address. 53 | const balances = await bitboxNetwork.getAllSlpBalancesAndUtxos(fundingAddress) as SlpBalancesResult; 54 | console.log("BCH balance:", balances.satoshis_available_bch); 55 | 56 | // 2) Calculate the token quantity with decimal precision included 57 | const initialTokenQtyBN = (new BigNumber(initialTokenQty)).times(10 ** decimals); 58 | 59 | // 3) Set private keys 60 | balances!.nonSlpUtxos.forEach((txo) => txo.wif = fundingWif); 61 | 62 | // 4) Use "simpleTokenGenesis()" helper method 63 | const genesisTxid = await bitboxNetwork.simpleTokenGenesis( 64 | name, 65 | ticker, 66 | initialTokenQtyBN, 67 | documentUri, 68 | documentHash, 69 | decimals, 70 | tokenReceiverAddress, 71 | batonReceiverAddress, 72 | bchChangeReceiverAddress, 73 | balances!.nonSlpUtxos, 74 | ); 75 | console.log("GENESIS txn complete:", genesisTxid); 76 | })(); 77 | -------------------------------------------------------------------------------- /examples/4-genesis-token-type-NFT1-parent.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * 3 | * Example 4: Genesis for NFT1 Parent - These are used to create individual NFT1 children 4 | * 5 | * Instructions: 6 | * (1) - Send some BCH to simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu 7 | * or tBCH to slptest:qpwyc9jnwckntlpuslg7ncmhe2n423304ueqcyw80l 8 | * to fund the example. 9 | * (2) - Run `tsc && node ` just before script execution 10 | * (3) - Optional: Use vscode debugger w/ launch.json settings 11 | * 12 | * ************************************************************************************/ 13 | 14 | import { BigNumber } from "bignumber.js"; 15 | import * as BITBOXSDK from "bitbox-sdk"; 16 | import { GrpcClient } from "grpc-bchrpc-node"; 17 | import { BchdNetwork, GetRawTransactionsAsync, LocalValidator, SlpBalancesResult } from "../index"; 18 | 19 | const name = "My NFT1 Group"; 20 | const ticker = "NFT1 Group"; 21 | const documentUri = null; 22 | const documentHash: Buffer|null = null; 23 | const initialTokenQty = 1000000; 24 | 25 | (async () => { 26 | 27 | // // NETWORK: FOR MAINNET UNCOMMENT 28 | const BITBOX = new BITBOXSDK.BITBOX(); 29 | const fundingAddress = "simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu"; // <-- must be simpleledger format 30 | const fundingWif = "L3gngkDg1HW5P9v5GdWWiCi3DWwvw5XnzjSPwNwVPN5DSck3AaiF"; // <-- compressed WIF format 31 | const tokenReceiverAddress = "simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu"; // <-- must be simpleledger format 32 | const bchChangeReceiverAddress = "simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu"; // <-- cashAddr or slpAddr format 33 | // For unlimited issuance provide a "batonReceiverAddress" 34 | const batonReceiverAddress = "simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu"; 35 | 36 | // VALIDATOR SETUP: FOR REMOTE VALIDATION 37 | const client = new GrpcClient(); //{ url: "bchd.ny1.simpleledger.io" }); 38 | 39 | const getRawTransactions: GetRawTransactionsAsync = async (txids: string[]) => { 40 | const txid = txids[0]; 41 | const res = await client.getRawTransaction({ hash: txid, reversedHashOrder: true }); 42 | return [Buffer.from(res.getTransaction_asU8()).toString("hex")]; 43 | }; 44 | const logger = console; 45 | const validator = new LocalValidator(BITBOX, getRawTransactions, logger); 46 | const bchdNetwork = new BchdNetwork({ BITBOX, client, validator, logger }); 47 | 48 | // 1) Get all balances at the funding address. 49 | const balances = await bchdNetwork.getAllSlpBalancesAndUtxos(fundingAddress) as SlpBalancesResult; 50 | console.log("BCH balance:", balances.satoshis_available_bch); 51 | 52 | // 2) Calculate the token quantity with decimal precision included 53 | const initialTokenQtyBN = new BigNumber(initialTokenQty); 54 | 55 | // 3) Set private keys 56 | balances!.nonSlpUtxos.forEach(txo => txo.wif = fundingWif); 57 | 58 | // 4) Use "simpleTokenGenesis()" helper method 59 | const genesisTxid = await bchdNetwork.simpleNFT1ParentGenesis( 60 | name, 61 | ticker, 62 | initialTokenQtyBN, 63 | documentUri, 64 | documentHash, 65 | tokenReceiverAddress, 66 | batonReceiverAddress, 67 | bchChangeReceiverAddress, 68 | balances!.nonSlpUtxos, 69 | ); 70 | console.log("NFT1 Parent GENESIS txn complete:", genesisTxid); 71 | })(); 72 | -------------------------------------------------------------------------------- /examples/5-genesis-token-type-NFT1-child.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * 3 | * Example 5: Genesis for a single NFT1 Child - Must have NFT1 parent first 4 | * 5 | * Instructions: 6 | * (1) - Send some BCH to simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu 7 | * or tBCH to slptest:qpwyc9jnwckntlpuslg7ncmhe2n423304ueqcyw80l 8 | * to fund the example. 9 | * (2) - Run `tsc && node ` just before script execution 10 | * (3) - Optional: Use a custom SLP validator by using GrpcClient "url" parameter 11 | * (4) - Optional: Use vscode debugger w/ launch.json settings 12 | * 13 | * ************************************************************************************/ 14 | 15 | import { BigNumber } from "bignumber.js"; 16 | import * as BITBOXSDK from "bitbox-sdk"; 17 | import { GrpcClient } from "grpc-bchrpc-node"; 18 | import { GetRawTransactionsAsync, 19 | LocalValidator, 20 | SlpAddressUtxoResult, 21 | SlpBalancesResult } from "../index"; 22 | import { BchdNetwork } from "../lib/bchdnetwork"; 23 | 24 | const name = "My NFT1 Child"; 25 | const ticker = "NFT1 Child"; 26 | const documentUri: string|null = null; 27 | const documentHash: Buffer|null = null; 28 | const NFT1ParentGroupID = "240c44216936e86e624538866934c6f038a6cc4a5a83db232d735f15e400b7ad"; 29 | 30 | (async () => { 31 | 32 | const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); 33 | 34 | // // NETWORK: FOR MAINNET UNCOMMENT 35 | const BITBOX = new BITBOXSDK.BITBOX(); 36 | const fundingAddress = "simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu"; // <-- must be simpleledger format 37 | const fundingWif = "L3gngkDg1HW5P9v5GdWWiCi3DWwvw5XnzjSPwNwVPN5DSck3AaiF"; // <-- compressed WIF format 38 | const tokenReceiverAddress = "simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu"; // <-- must be simpleledger format 39 | const bchChangeReceiverAddress = "simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu"; // <-- cashAddr or slpAddr format 40 | 41 | // VALIDATOR SETUP: FOR REMOTE VALIDATION 42 | const client = new GrpcClient({ url: "bchd.greyh.at:8335" }); 43 | const getRawTransactions: GetRawTransactionsAsync = async (txids: string[]) => { 44 | const txid = txids[0]; 45 | const res = await client.getRawTransaction({ hash: txid, reversedHashOrder: true }); 46 | return [Buffer.from(res.getTransaction_asU8()).toString("hex")]; 47 | }; 48 | const logger = console; 49 | const validator = new LocalValidator(BITBOX, getRawTransactions, logger); 50 | const bchdNetwork = new BchdNetwork({ BITBOX, client, validator }); 51 | 52 | // Get all balances at the funding address. 53 | let balances = await bchdNetwork.getAllSlpBalancesAndUtxos(fundingAddress) as SlpBalancesResult; 54 | console.log("BCH balance:", balances.satoshis_available_bch); 55 | 56 | // Look at the NFT1 Parent token balance. Make sure its greater than 0. 57 | if (!balances.slpTokenBalances[NFT1ParentGroupID] || 58 | !balances.slpTokenBalances[NFT1ParentGroupID].isGreaterThan(0)) { 59 | throw Error("Insufficient balance of NFT1 tokens, first you need to create NFT1 parent at this address."); 60 | } 61 | 62 | // Try to find an NFT parent that has quantity equal to 1 63 | let nftGroupUtxo: SlpAddressUtxoResult|undefined; 64 | balances.slpTokenUtxos[NFT1ParentGroupID].forEach((txo) => { 65 | if (!nftGroupUtxo && txo.slpUtxoJudgementAmount.isEqualTo(1)) { 66 | nftGroupUtxo = txo; 67 | } 68 | }); 69 | 70 | // If there wasn't any NFT1 parent UTXO with quantity of 1, so we create a TXO w/ qty 1 to be burned. 71 | if (!nftGroupUtxo) { 72 | const inputs = [...balances.nonSlpUtxos, ...balances.slpTokenUtxos[NFT1ParentGroupID]]; 73 | inputs.map((txo) => txo.wif = fundingWif); 74 | const sendTxid = await bchdNetwork.simpleTokenSend( 75 | NFT1ParentGroupID, new BigNumber(1), inputs, 76 | tokenReceiverAddress, tokenReceiverAddress); 77 | 78 | // wait for transaction to hit the full node. 79 | console.log("Created new parent UTXO to burn:", sendTxid); 80 | console.log("Waiting for the Full Node to sync with transaction..."); 81 | await sleep(3000); 82 | 83 | // update balances and set the newly created parent TXO. 84 | balances = (await bchdNetwork.getAllSlpBalancesAndUtxos(fundingAddress) as SlpBalancesResult); 85 | balances.slpTokenUtxos[NFT1ParentGroupID].forEach(txo => { 86 | if (!nftGroupUtxo && txo.slpUtxoJudgementAmount.isEqualTo(1)) { 87 | nftGroupUtxo = txo; 88 | } 89 | }); 90 | } 91 | 92 | // 3) Set private keys 93 | const inputs = [nftGroupUtxo!, ...balances.nonSlpUtxos]; 94 | inputs.map((txo) => txo.wif = fundingWif); 95 | 96 | // 4) Use "simpleNFT1ChildGenesis()" helper method 97 | const genesisTxid = await bchdNetwork.simpleNFT1ChildGenesis( 98 | NFT1ParentGroupID, 99 | name, 100 | ticker, 101 | documentUri, 102 | documentHash, 103 | tokenReceiverAddress, 104 | bchChangeReceiverAddress, 105 | inputs, 106 | ); 107 | 108 | console.log("NFT1 Child GENESIS txn complete:", genesisTxid); 109 | })(); 110 | -------------------------------------------------------------------------------- /examples/6-mint-token.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * 3 | * Example 6: Minting for a Type 1 or NFT1 Parent token 4 | * 5 | * Instructions: 6 | * (1) - Send some BCH to simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu 7 | * or tBCH to slptest:qpwyc9jnwckntlpuslg7ncmhe2n423304ueqcyw80l 8 | * (2) - Select Network and Address by commenting/uncommenting the desired 9 | * NETWORK section and providing valid BCH address. 10 | * 11 | * (3) - Select a Validation method by commenting/uncommenting the desired 12 | * VALIDATOR section. Chose from remote validator or local validator. 13 | * Both options rely on remote JSON RPC calls to rest.bitcoin.com. 14 | * - Option 1: REMOTE VALIDATION (rest.bitcoin.com/v2/slp/isTxidValid/) 15 | * - Option 2: LOCAL VALIDATOR / REST JSON RPC 16 | * - Option 3: LOCAL VALIDATOR / LOCAL FULL NODE 17 | * 18 | * (4) - Run `tsc && node ` just before script execution, or for 19 | * debugger just run `tsc` in the console and then use vscode debugger 20 | * with "Launch Current File" mode. 21 | * 22 | * ************************************************************************************/ 23 | 24 | import { BigNumber } from "bignumber.js"; 25 | import * as BITBOXSDK from "bitbox-sdk"; 26 | import { GetRawTransactionRequest, GrpcClient } from "grpc-bchrpc-node"; 27 | import { BitboxNetwork, LocalValidator, SlpBalancesResult } from "../index"; 28 | 29 | (async () => { 30 | 31 | // FOR MAINNET UNCOMMENT 32 | const BITBOX = new BITBOXSDK.BITBOX({ restURL: "https://rest.bitcoin.com/v2/" }); 33 | const fundingAddress = "simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu"; // <-- must be simpleledger format 34 | const fundingWif = "L3gngkDg1HW5P9v5GdWWiCi3DWwvw5XnzjSPwNwVPN5DSck3AaiF"; // <-- compressed WIF format 35 | const tokenReceiverAddress = "simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu"; // <-- must be simpleledger format 36 | const batonReceiverAddress = "simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu"; 37 | const bchChangeReceiverAddress = "simpleledger:qrhvcy5xlegs858fjqf8ssl6a4f7wpstaqnt0wauwu"; // <-- cashAddr or slpAddr format 38 | const tokenIdHexToMint = "112f967519e18083c8e4bd7ba67ebc04d72aaaa941826d38655c53d677e6a5be"; 39 | const additionalTokenQty = 1000; 40 | 41 | // FOR TESTNET UNCOMMENT 42 | // const BITBOX = new BITBOXSDK.BITBOX({ restURL: 'https://trest.bitcoin.com/v2/' }); 43 | // const fundingAddress = "slptest:qpwyc9jnwckntlpuslg7ncmhe2n423304ueqcyw80l"; 44 | // const fundingWif = "cVjzvdHGfQDtBEq7oddDRcpzpYuvNtPbWdi8tKQLcZae65G4zGgy"; 45 | // const tokenReceiverAddress = "slptest:qpwyc9jnwckntlpuslg7ncmhe2n423304ueqcyw80l"; 46 | // const batonReceiverAddress = "slptest:qpwyc9jnwckntlpuslg7ncmhe2n423304ueqcyw80l"; 47 | // const bchChangeReceiverAddress = "slptest:qpwyc9jnwckntlpuslg7ncmhe2n423304ueqcyw80l"; 48 | // const tokenIdHexToMint = "a67e2abb2fcfaa605c6a3b0dfb642cc830b63138d85b5e95eee523fdbded4d74"; 49 | // let additionalTokenQty = 1000 50 | 51 | // VALIDATOR: Option 1: FOR REMOTE VALIDATION 52 | //const bitboxNetwork = new BitboxNetwork(BITBOX); 53 | 54 | // VALIDATOR: Option 2: FOR LOCAL VALIDATOR / REMOTE JSON RPC 55 | // const getRawTransactions: GetRawTransactionsAsync = async function(txids: string[]) { 56 | // return await BITBOX.RawTransactions.getRawTransaction(txids) 57 | // } 58 | // const logger = console; 59 | // const slpValidator = new LocalValidator(BITBOX, getRawTransactions, logger); 60 | // const bitboxNetwork = new BitboxNetwork(BITBOX, slpValidator); 61 | 62 | // VALIDATOR: Option 3: LOCAL VALIDATOR / LOCAL FULL NODE JSON RPC 63 | const logger = console; 64 | const RpcClient = require("bitcoin-rpc-promise"); 65 | const connectionString = "http://bitcoin:password@localhost:8332" 66 | const rpc = new RpcClient(connectionString); 67 | const slpValidator = new LocalValidator(BITBOX, async (txids) => [ await rpc.getRawTransaction(txids[0]) ], logger); 68 | const bitboxNetwork = new BitboxNetwork(BITBOX, slpValidator); 69 | 70 | // 1) Get all balances at the funding address. 71 | const balances = await bitboxNetwork.getAllSlpBalancesAndUtxos(fundingAddress) as SlpBalancesResult; 72 | if (balances.slpBatonUtxos[tokenIdHexToMint]) { 73 | console.log("You have the minting baton for this token"); 74 | } else { 75 | throw Error("You don't have the minting baton for this token"); 76 | } 77 | 78 | // 2) Fetch critical token decimals information using bitdb 79 | const tokenInfo = await bitboxNetwork.getTokenInformation(tokenIdHexToMint); 80 | const tokenDecimals = tokenInfo.decimals; 81 | console.log("Token precision: " + tokenDecimals.toString()); 82 | 83 | // 3) Multiply the specified token quantity by 10^(token decimal precision) 84 | const mintQty = (new BigNumber(additionalTokenQty)).times(10 ** tokenDecimals); 85 | 86 | // 4) Filter the list to choose ONLY the baton of interest 87 | // NOTE: (spending other batons for other tokens will result in losing ability to mint those tokens) 88 | let inputUtxos = balances.slpBatonUtxos[tokenIdHexToMint]; 89 | 90 | // 5) Simply sweep our BCH (non-SLP) utxos to fuel the transaction 91 | inputUtxos = inputUtxos.concat(balances.nonSlpUtxos); 92 | 93 | // 6) Set the proper private key for each Utxo 94 | inputUtxos.forEach((txo) => txo.wif = fundingWif); 95 | 96 | // 7) MINT token using simple function 97 | const mintTxid = await bitboxNetwork.simpleTokenMint( 98 | tokenIdHexToMint, 99 | mintQty, 100 | inputUtxos, 101 | tokenReceiverAddress, 102 | batonReceiverAddress, 103 | bchChangeReceiverAddress, 104 | ); 105 | console.log("MINT txn complete:", mintTxid); 106 | 107 | })(); 108 | -------------------------------------------------------------------------------- /examples/7-send-token-p2pkh.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * 3 | * Example 7: Send any type of token. 4 | * 5 | * Instructions: 6 | * (1) - Select Network and Address by commenting/uncommenting the desired 7 | * NETWORK section and providing valid BCH address. 8 | * 9 | * (2) - Select a Validation method by commenting/uncommenting the desired 10 | * VALIDATOR section. Chose from remote validator or local validator. 11 | * Both options rely on remote JSON RPC calls to rest.bitcoin.com. 12 | * - Option 1: REMOTE VALIDATION (rest.bitcoin.com/v2/slp/isTxidValid/) 13 | * - Option 2: LOCAL VALIDATOR / REST JSON RPC 14 | * - Option 3: LOCAL VALIDATOR / LOCAL FULL NODE 15 | * 16 | * (3) - Run `tsc && node ` just before script execution, or for 17 | * debugger just run `tsc` in the console and then use vscode debugger 18 | * with "Launch Current File" mode. 19 | * 20 | * ************************************************************************************/ 21 | 22 | import * as BITBOXSDK from "bitbox-sdk"; 23 | import { BigNumber } from "bignumber.js"; 24 | import { BitboxNetwork, SlpBalancesResult, GetRawTransactionsAsync, LocalValidator } from "../index"; 25 | 26 | (async () => { 27 | 28 | // FOR MAINNET UNCOMMENT 29 | const BITBOX = new BITBOXSDK.BITBOX({ restURL: "https://rest.bitcoin.com/v2/" }); 30 | const fundingAddress = "simpleledger:qzeq626k0w2sg50vrlhthm6988qtp93pnvfeppy4ze"; // <-- must be simpleledger format 31 | const fundingWif = "L56PzQUUt2Cf8rh8GZwv6uEqwm4avxtHUeZaiBAN8ayRCaSrJTNW"; // <-- compressed WIF format 32 | const tokenReceiverAddress = [ "simpleledger:qpq8swcqnmvvaym6az5tlr377ukwem3ykupshhwvz3" ]; // <-- must be simpleledger format 33 | const bchChangeReceiverAddress = "simpleledger:qzeq626k0w2sg50vrlhthm6988qtp93pnvfeppy4ze"; // <-- must be simpleledger format 34 | const tokenId = "d6876f0fce603be43f15d34348bb1de1a8d688e1152596543da033a060cff798"; 35 | const sendAmounts = [ 0.000001 ]; 36 | 37 | // FOR TESTNET UNCOMMENT 38 | // const BITBOX = new BITBOXSDK.BITBOX({ restURL: 'https://trest.bitcoin.com/v2/' }); 39 | // const fundingAddress = "slptest:qpwyc9jnwckntlpuslg7ncmhe2n423304ueqcyw80l"; // <-- must be simpleledger format 40 | // const fundingWif = "cVjzvdHGfQDtBEq7oddDRcpzpYuvNtPbWdi8tKQLcZae65G4zGgy"; // <-- compressed WIF format 41 | // const tokenReceiverAddress = "slptest:qpwyc9jnwckntlpuslg7ncmhe2n423304ueqcyw80l"; // <-- must be simpleledger format 42 | // const bchChangeReceiverAddress = "slptest:qpwyc9jnwckntlpuslg7ncmhe2n423304ueqcyw80l"; // <-- must be simpleledger format 43 | // let tokenId = "78d57a82a0dd9930cc17843d9d06677f267777dd6b25055bad0ae43f1b884091"; 44 | // let sendAmounts = [ 10 ]; 45 | 46 | // VALIDATOR: Option 1: FOR REMOTE VALIDATION 47 | const bitboxNetwork = new BitboxNetwork(BITBOX); 48 | 49 | // VALIDATOR: Option 2: FOR LOCAL VALIDATOR / REMOTE JSON RPC 50 | // const getRawTransactions: GetRawTransactionsAsync = async function(txids: string[]) { 51 | // return await BITBOX.RawTransactions.getRawTransaction(txids) 52 | // } 53 | // const logger = console; 54 | // const slpValidator = new LocalValidator(BITBOX, getRawTransactions, logger); 55 | // const bitboxNetwork = new BitboxNetwork(BITBOX, slpValidator); 56 | 57 | // VALIDATOR: Option 3: LOCAL VALIDATOR / LOCAL FULL NODE JSON RPC 58 | // const logger = console; 59 | // const RpcClient = require("bitcoin-rpc-promise"); 60 | // const connectionString = "http://bitcoin:password@localhost:8332"; 61 | // const rpc = new RpcClient(connectionString); 62 | // const slpValidator = new LocalValidator(BITBOX, async (txids) => [ await rpc.getRawTransaction(txids[0]) ], logger); 63 | // const bitboxNetwork = new BitboxNetwork(BITBOX, slpValidator); 64 | 65 | // 1) Fetch token information 66 | const tokenInfo = await bitboxNetwork.getTokenInformation(tokenId); 67 | const tokenDecimals = tokenInfo.decimals; 68 | console.log("Token precision: " + tokenDecimals.toString()); 69 | 70 | // 2) Check that token balance is greater than our desired sendAmount 71 | const balances = await bitboxNetwork.getAllSlpBalancesAndUtxos(fundingAddress) as SlpBalancesResult; 72 | console.log(balances); 73 | if (balances.slpTokenBalances[tokenId] === undefined) { 74 | console.log("You need to fund the addresses provided in this example with tokens and BCH. Change the tokenId as required.") 75 | } 76 | console.log("Token balance:", balances.slpTokenBalances[tokenId].toFixed() as any / 10 ** tokenDecimals); 77 | 78 | // 3) Calculate send amount in "Token Satoshis". In this example we want to just send 1 token unit to someone... 79 | const sendAmountsBN = sendAmounts.map(a => (new BigNumber(a)).times(10**tokenDecimals)); // Don't forget to account for token precision 80 | 81 | // 4) Get all of our token's UTXOs 82 | let inputUtxos = balances.slpTokenUtxos[tokenId]; 83 | 84 | // 5) Simply sweep our BCH utxos to fuel the transaction 85 | inputUtxos = inputUtxos.concat(balances.nonSlpUtxos); 86 | 87 | // 6) Set the proper private key for each Utxo 88 | inputUtxos.forEach((txo) => txo.wif = fundingWif); 89 | 90 | // 7) Send token 91 | const sendTxid = await bitboxNetwork.simpleTokenSend( 92 | tokenId, 93 | sendAmountsBN, 94 | inputUtxos, 95 | tokenReceiverAddress, 96 | bchChangeReceiverAddress, 97 | ); 98 | console.log("SEND txn complete:", sendTxid); 99 | 100 | })(); 101 | -------------------------------------------------------------------------------- /examples/8-send-token-p2sh-frozen.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * 3 | * Example 8: Send any type of token using P2SH frozen address 4 | * 5 | * redeemScript (locking script): 6 | * ` OP_CHECKLOCKTIMEVERIFY OP_DROP OP_CHECKSIG` 7 | * unlocking script: 8 | * `` 9 | * 10 | * Instructions: 11 | * (1) - Select Network and Address by commenting/uncommenting the desired 12 | * NETWORK section and providing valid BCH address. 13 | * (2) - Select a Validation method by commenting/uncommenting the desired 14 | * VALIDATOR section. Chose from remote validator or local validator. 15 | * Both options rely on remote JSON RPC calls to rest.bitcoin.com. 16 | * (3) - Run `tsc && node ` just before script execution 17 | * (4) - Optional: Use vscode debugger w/ launch.json settings 18 | * 19 | * ************************************************************************************/ 20 | 21 | import * as BITBOXSDK from 'bitbox-sdk'; 22 | import { BigNumber } from 'bignumber.js'; 23 | import { BitboxNetwork, SlpBalancesResult, Slp, TransactionHelpers, Utils } from '../index'; 24 | 25 | (async function() { 26 | 27 | const BITBOX = new BITBOXSDK.BITBOX({ restURL: 'https://trest.bitcoin.com/v2/' }); 28 | const slp = new Slp(BITBOX); 29 | const helpers = new TransactionHelpers(slp); 30 | const opcodes = BITBOX.Script.opcodes; 31 | 32 | const pubkey = "0286d74c6fb92cb7b70b817094f48bf8fd398e64663bc3ddd7acc0a709212b9f69"; 33 | const wif = "cPamRLmPyuuwgRAbB6JHhXvrGwvHtmw9LpVi8QnUZYBubCeyjgs1"; 34 | const tokenReceiverAddress = [ "slptest:prk685k6r508xkj7u9g8v9p3f97hrmgr2qp7r4safs" ]; // <-- must be simpleledger format 35 | const bchChangeReceiverAddress = "slptest:prk685k6r508xkj7u9g8v9p3f97hrmgr2qp7r4safs"; // <-- must be simpleledger format 36 | let tokenId = "f0c7a8a7addc29edbc193212057d91c3eb004678e15e4662773146bdd51f8d7a"; 37 | let sendAmounts = [ 1 ]; 38 | 39 | // Set our BIP-113 time lock (subtract an hour to acount for MTP-11) 40 | const time_delay_seconds = 0; // let's just set it to 0 so we can redeem it immediately. 41 | let locktime = (await BITBOX.Blockchain.getBlockchainInfo()).mediantime + time_delay_seconds - 3600; 42 | 43 | // NOTE: the following locktime is hard-coded so that we can reuse the same P2SH address. 44 | let locktimeBip62 = 'c808f05c' //slpjs.Utils.get_BIP62_locktime_hex(locktime); 45 | 46 | let redeemScript = BITBOX.Script.encode([ 47 | Buffer.from(locktimeBip62, 'hex'), 48 | opcodes.OP_CHECKLOCKTIMEVERIFY, 49 | opcodes.OP_DROP, 50 | Buffer.from(pubkey, 'hex'), 51 | opcodes.OP_CHECKSIG 52 | ]) 53 | 54 | // Calculate the address for this script contract 55 | // We need to send some token and BCH to it before we can spend it! 56 | let fundingAddress = Utils.slpAddressFromHash160( 57 | BITBOX.Crypto.hash160(redeemScript), 58 | 'testnet', 59 | 'p2sh' 60 | ); 61 | 62 | // gives us: slptest:prk685k6r508xkj7u9g8v9p3f97hrmgr2qp7r4safs 63 | 64 | const bitboxNetwork = new BitboxNetwork(BITBOX); 65 | 66 | // Fetch critical token information 67 | const tokenInfo = await bitboxNetwork.getTokenInformation(tokenId); 68 | let tokenDecimals = tokenInfo.decimals; 69 | console.log("Token precision: " + tokenDecimals.toString()); 70 | 71 | // Check that token balance is greater than our desired sendAmount 72 | let balances = await bitboxNetwork.getAllSlpBalancesAndUtxos(fundingAddress); 73 | console.log(balances); 74 | if(balances.slpTokenBalances[tokenId] === undefined) 75 | console.log("You need to fund the addresses provided in this example with tokens and BCH. Change the tokenId as required.") 76 | console.log("Token balance:", balances.slpTokenBalances[tokenId].toFixed() / 10**tokenDecimals); 77 | 78 | // Calculate send amount in "Token Satoshis". In this example we want to just send 1 token unit to someone... 79 | let sendAmountsBN = sendAmounts.map(a => (new BigNumber(a)).times(10**tokenDecimals)); // Don't forget to account for token precision 80 | 81 | // Get all of our token's UTXOs 82 | let inputUtxos = balances.slpTokenUtxos[tokenId]; 83 | 84 | // Simply sweep our BCH utxos to fuel the transaction 85 | inputUtxos = inputUtxos.concat(balances.nonSlpUtxos); 86 | 87 | // Estimate the additional fee for our larger p2sh scriptSigs 88 | let extraFee = (8) * // for OP_CTLV and timelock data push 89 | inputUtxos.length // this many times since we swept inputs from p2sh address 90 | 91 | // Build an unsigned transaction 92 | let unsignedTxnHex = helpers.simpleTokenSend({ tokenId, sendAmounts: sendAmountsBN, inputUtxos, tokenReceiverAddresses: tokenReceiverAddress, changeReceiverAddress: bchChangeReceiverAddress, extraFee }); 93 | unsignedTxnHex = helpers.enableInputsCLTV(unsignedTxnHex); 94 | unsignedTxnHex = helpers.setTxnLocktime(unsignedTxnHex, locktime); 95 | 96 | // Build scriptSigs 97 | let scriptSigs = inputUtxos.map((txo, i) => { 98 | let sigObj = helpers.get_transaction_sig_p2sh(unsignedTxnHex, wif, i, txo.satoshis, redeemScript, redeemScript) 99 | return { 100 | index: i, 101 | lockingScriptBuf: redeemScript, 102 | unlockingScriptBufArray: [ sigObj.signatureBuf ] 103 | } 104 | }) 105 | 106 | let signedTxn = helpers.addScriptSigs(unsignedTxnHex, scriptSigs); 107 | 108 | // 11) Send token 109 | let sendTxid = await bitboxNetwork.sendTx(signedTxn) 110 | console.log("SEND txn complete:", sendTxid); 111 | })(); 112 | -------------------------------------------------------------------------------- /examples/9-send-token-p2sh-multisig.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * 3 | * Example 9: Send any type of token using P2SH multisig 4 | * 5 | * Instructions: 6 | * (1) - Select Network and Address by commenting/uncommenting the desired 7 | * NETWORK section and providing valid BCH address. 8 | * (2) - Select a Validation method by commenting/uncommenting the desired 9 | * VALIDATOR section. Chose from remote validator or local validator. 10 | * Both options rely on remote JSON RPC calls to rest.bitcoin.com. 11 | * (3) - Run `tsc && node ` just before script execution 12 | * (4) - Optional: Use vscode debugger w/ launch.json settings 13 | * 14 | * ************************************************************************************/ 15 | 16 | import * as BITBOXSDK from 'bitbox-sdk'; 17 | import { BigNumber } from 'bignumber.js'; 18 | import { BitboxNetwork, SlpBalancesResult, Slp, TransactionHelpers } from '../index'; 19 | 20 | (async function() { 21 | 22 | const BITBOX = new BITBOXSDK.BITBOX({ restURL: 'https://rest.bitcoin.com/v2/' }); 23 | const slp = new Slp(BITBOX); 24 | const helpers = new TransactionHelpers(slp); 25 | 26 | const pubkey_signer_1 = "02471e07bcf7d47afd40e0bf4f806347f9e8af4dfbbb3c45691bbaefd4ea926307"; // Signer #1 27 | const pubkey_signer_2 = "03472cfca5da3bf06a85c5fd860ffe911ef374cf2a9b754fd861d1ead668b15a32"; // Signer #2 28 | 29 | // we have two signers for this 2-of-2 multisig address (so for the missing key we can just put "null") 30 | const wifs = ["Ky6iiLSL2K9stMd4G5dLeXfpVKu5YRB6dhjCsHyof3eaB2cDngSr", "L2AdfmxwsYu3KnRASZ51C3UEnduUDy1b21sSF9JbLNVEPzsxEZib"] //[ "Ky6iiLSL2K9stMd4G5dLeXfpVKu5YRB6dhjCsHyof3eaB2cDngSr", null ]; 31 | 32 | // to keep this example alive we will just send everything to the same address 33 | const tokenReceiverAddress = [ "simpleledger:pphnuh7dx24rcwjkj0sl6xqfyfzf23aj7udr0837gn" ]; // <-- must be simpleledger format 34 | const bchChangeReceiverAddress = "simpleledger:pphnuh7dx24rcwjkj0sl6xqfyfzf23aj7udr0837gn"; // <-- must be simpleledger format 35 | let tokenId = "497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7"; 36 | var sendAmounts: number[]|BigNumber[] = [ 1 ]; 37 | 38 | const bitboxNetwork = new BitboxNetwork(BITBOX); 39 | 40 | // 1) Fetch critical token information 41 | const tokenInfo = await bitboxNetwork.getTokenInformation(tokenId); 42 | let tokenDecimals = tokenInfo.decimals; 43 | console.log("Token precision: " + tokenDecimals.toString()); 44 | 45 | // Wait for network responses... 46 | 47 | // 2) Check that token balance is greater than our desired sendAmount 48 | let fundingAddress = "simpleledger:pphnuh7dx24rcwjkj0sl6xqfyfzf23aj7udr0837gn"; 49 | let balances = await bitboxNetwork.getAllSlpBalancesAndUtxos(fundingAddress); 50 | console.log(balances); 51 | if(balances.slpTokenBalances[tokenId] === undefined) 52 | console.log("You need to fund the addresses provided in this example with tokens and BCH. Change the tokenId as required.") 53 | console.log("Token balance:", balances.slpTokenBalances[tokenId].toFixed() / 10**tokenDecimals); 54 | 55 | // Wait for network responses... 56 | 57 | // 3) Calculate send amount in "Token Satoshis". In this example we want to just send 1 token unit to someone... 58 | sendAmounts = sendAmounts.map(a => (new BigNumber(a)).times(10**tokenDecimals)); // Don't forget to account for token precision 59 | 60 | // 4) Get all of our token's UTXOs 61 | let inputUtxos = balances.slpTokenUtxos[tokenId]; 62 | 63 | // 5) Simply sweep our BCH utxos to fuel the transaction 64 | inputUtxos = inputUtxos.concat(balances.nonSlpUtxos); 65 | 66 | // 6) Estimate the additional fee for our larger p2sh scriptSigs 67 | let extraFee = (2 * 33 + // two pub keys in each redeemScript 68 | 2 * 72 + // two signatures in scriptSig 69 | 10) * // for OP_CMS and various length bytes 70 | inputUtxos.length // this many times since we swept inputs from p2sh address 71 | 72 | // 7) Build an unsigned transaction 73 | let unsignedTxnHex = helpers.simpleTokenSend({ tokenId, sendAmounts, inputUtxos, tokenReceiverAddresses: tokenReceiverAddress, changeReceiverAddress: bchChangeReceiverAddress, extraFee }); 74 | 75 | // 8) Build scriptSigs for all intputs 76 | let redeemData = helpers.build_P2SH_multisig_redeem_data(2, [pubkey_signer_1, pubkey_signer_2]); 77 | let scriptSigs = inputUtxos.map((txo, i) => { 78 | let sigData = redeemData.pubKeys.map((pk, j) => { 79 | if(wifs[j]) { 80 | return helpers.get_transaction_sig_p2sh(unsignedTxnHex, wifs[j]!, i, txo.satoshis, redeemData.lockingScript, redeemData.lockingScript) 81 | } 82 | else { 83 | return helpers.get_transaction_sig_filler(i, pk) 84 | } 85 | }) 86 | return helpers.build_P2SH_multisig_scriptSig(redeemData, i, sigData) 87 | }) 88 | 89 | // 9) apply our scriptSigs to the unsigned transaction 90 | let signedTxn = helpers.addScriptSigs(unsignedTxnHex, scriptSigs); 91 | 92 | // 10) OPTIONAL: Update transaction hex with input values to allow for our second signer who is using Electron Cash SLP edition (https://simpleledger.cash/project/electron-cash-slp-edition/) 93 | //let input_values = inputUtxos.map(txo => txo.satoshis) 94 | //signedTxn = helpers.insert_input_values_for_EC_signers(signedTxn, input_values) 95 | 96 | // 11) Send token 97 | let sendTxid = await bitboxNetwork.sendTx(signedTxn) 98 | console.log("SEND txn complete:", sendTxid); 99 | 100 | })(); 101 | -------------------------------------------------------------------------------- /examples/9-send-token-p2sh-p2pkh.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * 3 | * Example 9: Send any type of token using P2SH multisig 4 | * 5 | * Instructions: 6 | * (1) - Select Network and Address by commenting/uncommenting the desired 7 | * NETWORK section and providing valid BCH address. 8 | * (2) - Select a Validation method by commenting/uncommenting the desired 9 | * VALIDATOR section. Chose from remote validator or local validator. 10 | * Both options rely on remote JSON RPC calls to rest.bitcoin.com. 11 | * (3) - Run `tsc && node ` just before script execution 12 | * (4) - Optional: Use vscode debugger w/ launch.json settings 13 | * 14 | * ************************************************************************************/ 15 | 16 | import * as BITBOXSDK from 'bitbox-sdk'; 17 | import { BigNumber } from 'bignumber.js'; 18 | import { BitboxNetwork, SlpBalancesResult, Slp, TransactionHelpers } from '../index'; 19 | 20 | (async function() { 21 | 22 | const BITBOX = new BITBOXSDK.BITBOX({ restURL: 'https://rest.bitcoin.com/v2/' }); 23 | const slp = new Slp(BITBOX); 24 | const helpers = new TransactionHelpers(slp); 25 | 26 | const pubkey_signer_1 = "02471e07bcf7d47afd40e0bf4f806347f9e8af4dfbbb3c45691bbaefd4ea926307"; // Signer #1 27 | const pubkey_signer_2 = "03472cfca5da3bf06a85c5fd860ffe911ef374cf2a9b754fd861d1ead668b15a32"; // Signer #2 28 | 29 | // we have two signers for this 2-of-2 multisig address (so for the missing key we can just put "null") 30 | const wifs = ["Ky6iiLSL2K9stMd4G5dLeXfpVKu5YRB6dhjCsHyof3eaB2cDngSr", "L2AdfmxwsYu3KnRASZ51C3UEnduUDy1b21sSF9JbLNVEPzsxEZib"] //[ "Ky6iiLSL2K9stMd4G5dLeXfpVKu5YRB6dhjCsHyof3eaB2cDngSr", null ]; 31 | 32 | // to keep this example alive we will just send everything to the same address 33 | const tokenReceiverAddress = [ "simpleledger:pphnuh7dx24rcwjkj0sl6xqfyfzf23aj7udr0837gn" ]; // <-- must be simpleledger format 34 | const bchChangeReceiverAddress = "simpleledger:pphnuh7dx24rcwjkj0sl6xqfyfzf23aj7udr0837gn"; // <-- must be simpleledger format 35 | let tokenId = "497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7"; 36 | var sendAmounts: number[]|BigNumber[] = [ 1 ]; 37 | 38 | const bitboxNetwork = new BitboxNetwork(BITBOX); 39 | 40 | // 1) Fetch critical token information 41 | const tokenInfo = await bitboxNetwork.getTokenInformation(tokenId); 42 | let tokenDecimals = tokenInfo.decimals; 43 | console.log("Token precision: " + tokenDecimals.toString()); 44 | 45 | // Wait for network responses... 46 | 47 | // 2) Check that token balance is greater than our desired sendAmount 48 | let fundingAddress = "simpleledger:pphnuh7dx24rcwjkj0sl6xqfyfzf23aj7udr0837gn"; 49 | let balances = await bitboxNetwork.getAllSlpBalancesAndUtxos(fundingAddress); 50 | console.log(balances); 51 | if(balances.slpTokenBalances[tokenId] === undefined) 52 | console.log("You need to fund the addresses provided in this example with tokens and BCH. Change the tokenId as required.") 53 | console.log("Token balance:", balances.slpTokenBalances[tokenId].toFixed() / 10**tokenDecimals); 54 | 55 | // Wait for network responses... 56 | 57 | // 3) Calculate send amount in "Token Satoshis". In this example we want to just send 1 token unit to someone... 58 | sendAmounts = sendAmounts.map(a => (new BigNumber(a)).times(10**tokenDecimals)); // Don't forget to account for token precision 59 | 60 | // 4) Get all of our token's UTXOs 61 | let inputUtxos = balances.slpTokenUtxos[tokenId]; 62 | 63 | // 5) Simply sweep our BCH utxos to fuel the transaction 64 | inputUtxos = inputUtxos.concat(balances.nonSlpUtxos); 65 | 66 | // 6) Estimate the additional fee for our larger p2sh scriptSigs 67 | let extraFee = (2 * 33 + // two pub keys in each redeemScript 68 | 2 * 72 + // two signatures in scriptSig 69 | 10) * // for OP_CMS and various length bytes 70 | inputUtxos.length // this many times since we swept inputs from p2sh address 71 | 72 | // 7) Build an unsigned transaction 73 | let unsignedTxnHex = helpers.simpleTokenSend({ tokenId, sendAmounts, inputUtxos, tokenReceiverAddresses: tokenReceiverAddress, changeReceiverAddress: bchChangeReceiverAddress, extraFee }); 74 | 75 | // 8) Build scriptSigs for all intputs 76 | let redeemData = helpers.build_P2SH_multisig_redeem_data(2, [pubkey_signer_1, pubkey_signer_2]); 77 | let scriptSigs = inputUtxos.map((txo, i) => { 78 | let sigData = redeemData.pubKeys.map((pk, j) => { 79 | if(wifs[j]) { 80 | return helpers.get_transaction_sig_p2sh(unsignedTxnHex, wifs[j]!, i, txo.satoshis, redeemData.lockingScript, redeemData.lockingScript) 81 | } 82 | else { 83 | return helpers.get_transaction_sig_filler(i, pk) 84 | } 85 | }) 86 | return helpers.build_P2SH_multisig_scriptSig(redeemData, i, sigData) 87 | }) 88 | 89 | // 9) apply our scriptSigs to the unsigned transaction 90 | let signedTxn = helpers.addScriptSigs(unsignedTxnHex, scriptSigs); 91 | 92 | // 10) OPTIONAL: Update transaction hex with input values to allow for our second signer who is using Electron Cash SLP edition (https://simpleledger.cash/project/electron-cash-slp-edition/) 93 | //let input_values = inputUtxos.map(txo => txo.satoshis) 94 | //signedTxn = helpers.insert_input_values_for_EC_signers(signedTxn, input_values) 95 | 96 | // 11) Send token 97 | let sendTxid = await bitboxNetwork.sendTx(signedTxn) 98 | console.log("SEND txn complete:", sendTxid); 99 | 100 | })(); 101 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | export * from "./lib/slp"; 4 | export * from "./lib/utils"; 5 | export * from "./lib/crypto"; 6 | export * from "./lib/primatives"; 7 | export * from "./lib/bitdbnetwork"; 8 | export * from "./lib/localvalidator"; 9 | export * from "./lib/trustedvalidator"; 10 | export * from "./lib/bitboxnetwork"; 11 | export * from "./lib/bchdnetwork"; 12 | export * from "./lib/transactionhelpers"; 13 | export * from "./lib/bchdtrustedvalidator"; 14 | import * as bitcore from "bitcore-lib-cash"; 15 | export { bitcore }; 16 | 17 | export interface logger { 18 | log: (s: string) => any; 19 | } 20 | 21 | export enum SlpTransactionType { 22 | "GENESIS" = "GENESIS", 23 | "MINT" = "MINT", 24 | "SEND" = "SEND", 25 | } 26 | 27 | export enum SlpVersionType { 28 | "TokenVersionType1" = 1, 29 | "TokenVersionType1_NFT_Child" = 65, 30 | "TokenVersionType1_NFT_Parent" = 129, 31 | } 32 | 33 | // negative values are bad, 0 = NOT_SLP, positive values are a SLP (token or baton) 34 | export enum SlpUtxoJudgement { 35 | "UNKNOWN" = "UNKNOWN", 36 | "INVALID_BATON_DAG" = "INVALID_BATON_DAG", 37 | "INVALID_TOKEN_DAG" = "INVALID_TOKEN_DAG", 38 | "NOT_SLP" = "NOT_SLP", 39 | "SLP_TOKEN" = "SLP_TOKEN", 40 | "SLP_BATON" = "SLP_BATON", 41 | "UNSUPPORTED_TYPE" = "UNSUPPORTED_TYPE", 42 | } 43 | 44 | export interface SlpTransactionDetails { 45 | transactionType: SlpTransactionType; 46 | tokenIdHex: string; 47 | versionType: SlpVersionType; 48 | timestamp?: string; 49 | symbol: string; 50 | name: string; 51 | documentUri: string; 52 | documentSha256: Buffer|null; 53 | decimals: number; 54 | containsBaton: boolean; 55 | batonVout: number|null; 56 | genesisOrMintQuantity: BigNumber|null; 57 | sendOutputs?: BigNumber[]|null; 58 | } 59 | 60 | export interface SlpTxnDetailsResult extends TxnDetailsResult { 61 | tokenInfo: SlpTransactionDetails; 62 | tokenIsValid: boolean; 63 | tokenNftParentId: string; 64 | } 65 | 66 | export interface SlpBalancesResult { 67 | satoshis_available_bch: number; 68 | satoshis_in_slp_baton: number; 69 | satoshis_in_slp_token: number; 70 | satoshis_in_invalid_token_dag: number; 71 | satoshis_in_invalid_baton_dag: number; 72 | satoshis_in_unknown_token_type: number; 73 | slpTokenBalances: {[key: string]: BigNumber}; 74 | nftParentChildBalances: {[key: string]: {[key: string]: BigNumber}}; 75 | slpTokenUtxos: {[key: string]: SlpAddressUtxoResult[]}; 76 | slpBatonUtxos: {[key: string]: SlpAddressUtxoResult[]}; 77 | nonSlpUtxos: SlpAddressUtxoResult[]; 78 | invalidTokenUtxos: SlpAddressUtxoResult[]; 79 | invalidBatonUtxos: SlpAddressUtxoResult[]; 80 | unknownTokenTypeUtxos: SlpAddressUtxoResult[]; 81 | } 82 | 83 | export class SlpAddressUtxoResult { 84 | public txid!: string; 85 | public vout!: number; 86 | public scriptPubKey!: string; 87 | public amount!: number; 88 | public satoshis!: number; 89 | public value?: number; 90 | public height!: number; 91 | public confirmations!: number; 92 | public legacyAddress!: string; 93 | public cashAddress!: string; 94 | public wif!: string; 95 | public tx?: TxnDetailsDeep; 96 | public txBuf?: Buffer; 97 | public slpTransactionDetails!: SlpTransactionDetails; 98 | public slpUtxoJudgement: SlpUtxoJudgement = SlpUtxoJudgement.UNKNOWN; 99 | public slpUtxoJudgementAmount!: BigNumber; 100 | public nftParentId?: string; 101 | } 102 | 103 | export interface utxo { 104 | txid: string; 105 | vout: number; 106 | satoshis: BigNumber; 107 | wif?: string; 108 | slpTransactionDetails: SlpTransactionDetails; 109 | slpUtxoJudgement: SlpUtxoJudgement; 110 | slpUtxoJudgementAmount: BigNumber; 111 | } 112 | 113 | export interface ScriptPubKey { 114 | hex: string; 115 | asm: string; 116 | addresses: string[]; 117 | type: string; 118 | } 119 | 120 | export interface Vout { 121 | value: Number; 122 | n: number; 123 | scriptPubKey: ScriptPubKey; 124 | } 125 | 126 | export interface Vin { 127 | txid: string; 128 | sequence: number; 129 | n: number; 130 | scriptSig: ScriptSig; 131 | value: number; 132 | legacyAddress: string; 133 | cashAddress: string; 134 | } 135 | 136 | export interface ScriptSig { 137 | hex: string; 138 | asm: string; 139 | } 140 | 141 | // Needed more type details than available in BITBOX types 142 | export interface TxnDetailsDeep { 143 | txid: string; 144 | version: number; 145 | locktime: number; 146 | vin: Vin[]; 147 | vout: Vout[]; 148 | blockhash: string; 149 | blockheight: number; 150 | confirmations: number; 151 | time: number; 152 | blocktime: number; 153 | isCoinBase: boolean; 154 | valueOut: number; 155 | size: number; 156 | raw?: string|Buffer; 157 | } 158 | 159 | import BigNumber from "bignumber.js"; 160 | import { AddressDetailsResult, AddressUtxoResult, TxnDetailsResult } from "bitcoin-com-rest"; 161 | import { Slp, SlpValidator } from "./lib/slp"; 162 | import { TransactionHelpers } from "./lib/transactionhelpers"; 163 | 164 | export interface INetwork extends SlpValidator { 165 | slp: Slp; 166 | validator?: SlpValidator; 167 | txnHelpers: TransactionHelpers; 168 | logger: logger; 169 | getNftParentId(tokenIdHex: string): Promise; 170 | getTokenInformation(txid: string, decimalConversion?: boolean): Promise; 171 | getTransactionDetails(txid: string, decimalConversion?: boolean): Promise; 172 | getUtxos(address: string): Promise; 173 | getAllSlpBalancesAndUtxos(address: string | string[]): Promise>; 177 | simpleTokenSend(tokenId: string, sendAmounts: BigNumber | BigNumber[], 178 | inputUtxos: SlpAddressUtxoResult[], tokenReceiverAddresses: string | string[], 179 | changeReceiverAddress: string, requiredNonTokenOutputs?: Array<{ 180 | satoshis: number; 181 | receiverAddress: string; 182 | }>): Promise; 183 | simpleBchSend(sendAmounts: BigNumber | BigNumber[], inputUtxos: SlpAddressUtxoResult[], 184 | bchReceiverAddresses: string | string[], changeReceiverAddress: string): Promise; 185 | simpleTokenGenesis(tokenName: string, tokenTicker: string, tokenAmount: BigNumber, 186 | documentUri: string | null, documentHash: Buffer | null, decimals: number, 187 | tokenReceiverAddress: string, batonReceiverAddress: string, bchChangeReceiverAddress: string, 188 | inputUtxos: SlpAddressUtxoResult[]): Promise; 189 | simpleNFT1ParentGenesis(tokenName: string, tokenTicker: string, tokenAmount: BigNumber, documentUri: string | null, 190 | documentHash: Buffer | null, tokenReceiverAddress: string, batonReceiverAddress: string, 191 | bchChangeReceiverAddress: string, inputUtxos: SlpAddressUtxoResult[], decimals?: number, 192 | ): Promise; 193 | simpleNFT1ChildGenesis(nft1GroupId: string, tokenName: string, tokenTicker: string, documentUri: string | null, 194 | documentHash: Buffer | null, tokenReceiverAddress: string, bchChangeReceiverAddress: string, 195 | inputUtxos: SlpAddressUtxoResult[], allowBurnAnyAmount?: boolean, 196 | ): Promise; 197 | simpleTokenMint(tokenId: string, mintAmount: BigNumber, inputUtxos: SlpAddressUtxoResult[], 198 | tokenReceiverAddress: string, batonReceiverAddress: string, changeReceiverAddress: string, 199 | ): Promise; 200 | simpleTokenBurn(tokenId: string, burnAmount: BigNumber, inputUtxos: SlpAddressUtxoResult[], 201 | changeReceiverAddress: string): Promise; 202 | getUtxoWithRetry(address: string, retries?: number): Promise; 203 | getUtxoWithTxDetails(address: string): Promise; 204 | getTransactionDetailsWithRetry(txids: string[], retries?: number): Promise; 205 | getAddressDetailsWithRetry(address: string, retries?: number): Promise; 206 | sendTx(hex: string): Promise; 207 | monitorForPayment(paymentAddress: string, fee: number, onPaymentCB: Function): Promise; 208 | getRawTransactions(txids: string[]): Promise; 209 | processUtxosForSlp(utxos: SlpAddressUtxoResult[]): Promise; 210 | isValidSlpTxid(txid: string): Promise; 211 | validateSlpTransactions(txids: string[]): Promise; 212 | } 213 | -------------------------------------------------------------------------------- /lib/bchdnetwork.ts: -------------------------------------------------------------------------------- 1 | import * as bchaddr from "bchaddrjs-slp"; 2 | import BigNumber from "bignumber.js"; 3 | import { BITBOX } from "bitbox-sdk"; 4 | import { AddressDetailsResult, AddressUtxoResult, 5 | TxnDetailsResult, utxo } from "bitcoin-com-rest"; 6 | import * as Bitcore from "bitcore-lib-cash"; 7 | import { IGrpcClient, SlpRequiredBurn, SubmitTransactionResponse, Transaction } from "grpc-bchrpc"; 8 | import * as _ from "lodash"; 9 | import { INetwork, logger, Primatives, 10 | SlpAddressUtxoResult, SlpBalancesResult, 11 | SlpTransactionDetails, SlpTransactionType, 12 | SlpTxnDetailsResult, SlpVersionType } from "../index"; 13 | import { Slp, SlpValidator } from "./slp"; 14 | import { TransactionHelpers } from "./transactionhelpers"; 15 | import { Utils } from "./utils"; 16 | 17 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 18 | 19 | export class BchdNetwork implements INetwork { 20 | public BITBOX: BITBOX; 21 | public slp: Slp; 22 | public validator: SlpValidator; 23 | public txnHelpers: TransactionHelpers; 24 | public logger: logger = { log: (s: string) => null }; 25 | public client: IGrpcClient; 26 | 27 | constructor({ BITBOX, validator, logger, client }: 28 | { BITBOX: BITBOX, client: IGrpcClient, validator: SlpValidator, logger?: logger }) { 29 | if (!BITBOX) { 30 | throw Error("Must provide BITBOX instance to class constructor."); 31 | } 32 | if (!client) { 33 | throw Error("Must provide instance of GrpClient to class constructor."); 34 | } 35 | if (logger) { 36 | this.logger = logger; 37 | } 38 | this.validator = validator; 39 | this.BITBOX = BITBOX; 40 | this.client = client; 41 | this.slp = new Slp(BITBOX); 42 | this.txnHelpers = new TransactionHelpers(this.slp); 43 | } 44 | 45 | public async getTokenInformation(txid: string, decimalConversion = false): Promise { 46 | let txhex: Buffer; 47 | txhex = Buffer.from((await this.client.getRawTransaction({hash: txid, reversedHashOrder: true })).getTransaction_asU8()); 48 | const txn: Bitcore.Transaction = new Bitcore.Transaction(txhex); 49 | const slpMsg = this.slp.parseSlpOutputScript(txn.outputs[0]._scriptBuffer); 50 | if (decimalConversion) { 51 | if ([SlpTransactionType.GENESIS, SlpTransactionType.MINT].includes(slpMsg.transactionType)) { 52 | slpMsg.genesisOrMintQuantity = slpMsg.genesisOrMintQuantity!.dividedBy(10 ** slpMsg.decimals); 53 | } else { 54 | slpMsg.sendOutputs!.map((o) => o.dividedBy(10 ** slpMsg.decimals)); 55 | } 56 | } 57 | if (SlpTransactionType.GENESIS === slpMsg.transactionType) { 58 | slpMsg.tokenIdHex = txid; 59 | } 60 | return slpMsg; 61 | } 62 | 63 | public async getTransactionDetails(txid: string): Promise { 64 | const res = await this.getTransactionDetailsWithRetry([txid], 1); 65 | if (!res) { 66 | return res; 67 | } 68 | return res[0]; 69 | } 70 | 71 | public async getUtxos(address: string): Promise { 72 | // must be a cash or legacy addr 73 | if (!bchaddr.isCashAddress(address) && !bchaddr.isLegacyAddress(address)) { 74 | throw new Error("Not an a valid address format, must be cashAddr or Legacy address format."); 75 | } 76 | const cashAddress = bchaddr.toCashAddress(address); 77 | const legacyAddress = bchaddr.toLegacyAddress(address); 78 | const res = (await this.client.getAddressUtxos({address, includeMempool: true})).getOutputsList(); 79 | if (res.length === 0) { 80 | return { 81 | cashAddress, 82 | legacyAddress, 83 | scriptPubKey: null, 84 | utxos: [], 85 | } as any as AddressUtxoResult; 86 | } 87 | const scriptPubKey = Buffer.from(res[0].getPubkeyScript_asU8()).toString("hex"); 88 | 89 | const bestHeight = (await this.client.getBlockchainInfo()).getBestHeight(); 90 | let utxos: utxo[] = []; 91 | if (res.length > 0) { 92 | utxos = res.map((txo: { getValue: () => number; getBlockHeight: () => number; getOutpoint: () => any; }) => { 93 | return { 94 | satoshis: txo.getValue(), 95 | height: txo.getBlockHeight() < 2147483647 ? txo.getBlockHeight() : -1, 96 | confirmations: txo.getBlockHeight() < 2147483647 ? bestHeight - txo.getBlockHeight() + 1 : null, 97 | txid: Buffer.from(txo.getOutpoint()!.getHash_asU8().reverse()).toString("hex"), 98 | vout: txo.getOutpoint()!.getIndex(), 99 | amount: txo.getValue() / 10 ** 8, 100 | } as any as utxo; 101 | }); 102 | } 103 | return { 104 | cashAddress, 105 | legacyAddress, 106 | scriptPubKey, 107 | utxos, 108 | }; 109 | } 110 | 111 | public async getAllSlpBalancesAndUtxos(address: string | string[]) 112 | : Promise> { 113 | if (typeof address === "string") { 114 | address = bchaddr.toCashAddress(address); 115 | const result = await this.getUtxoWithTxDetails(address); 116 | return this.processUtxosForSlp(result); 117 | } 118 | address = address.map((a) => bchaddr.toCashAddress(a)); 119 | const results: Array<{ address: string, result: SlpBalancesResult }> = []; 120 | for (const addr of address) { 121 | const utxos = await this.getUtxoWithTxDetails(addr); 122 | results.push({ address: Utils.toSlpAddress(addr), result: await this.processUtxosForSlp(utxos) }); 123 | } 124 | return results; 125 | } 126 | 127 | // Sent SLP tokens to a single output address with change handled 128 | // (Warning: Sweeps all BCH/SLP UTXOs for the funding address) 129 | public async simpleTokenSend( 130 | tokenId: string, sendAmounts: BigNumber|BigNumber[], inputUtxos: SlpAddressUtxoResult[], 131 | tokenReceiverAddresses: string|string[], changeReceiverAddress: string, requiredNonTokenOutputs 132 | : Array<{ satoshis: number, receiverAddress: string }> = []) { 133 | const txHex = this.txnHelpers.simpleTokenSend({ 134 | tokenId, sendAmounts, inputUtxos, tokenReceiverAddresses, 135 | changeReceiverAddress, requiredNonTokenOutputs, 136 | }); 137 | 138 | if (!inputUtxos.every((i) => typeof i.wif === "string")) { 139 | throw Error("The BitboxNetwork version of this method requires a private key WIF be provided with each input." + 140 | "If you want more control over the signing process use Slp.simpleTokenSend() to get the unsigned transaction," + 141 | "then after the transaction is signed you can use BitboxNetwork.sendTx()"); 142 | } 143 | 144 | return this.sendTx(txHex); 145 | } 146 | 147 | public async simpleBchSend( 148 | sendAmounts: BigNumber|BigNumber[], inputUtxos: SlpAddressUtxoResult[], 149 | bchReceiverAddresses: string|string[], changeReceiverAddress: string) { 150 | const genesisTxHex = this.txnHelpers.simpleBchSend({ 151 | sendAmounts, inputUtxos, bchReceiverAddresses, changeReceiverAddress, 152 | }); 153 | return this.sendTx(genesisTxHex); 154 | } 155 | 156 | public async simpleTokenGenesis( 157 | tokenName: string, tokenTicker: string, tokenAmount: BigNumber, documentUri: string|null, 158 | documentHash: Buffer|null, decimals: number, tokenReceiverAddress: string, batonReceiverAddress: string, 159 | bchChangeReceiverAddress: string, inputUtxos: SlpAddressUtxoResult[]) { 160 | const genesisTxHex = this.txnHelpers.simpleTokenGenesis({ 161 | tokenName, tokenTicker, tokenAmount, documentUri, documentHash, decimals, 162 | tokenReceiverAddress, batonReceiverAddress, bchChangeReceiverAddress, inputUtxos, 163 | }); 164 | return this.sendTx(genesisTxHex); 165 | } 166 | 167 | public async simpleNFT1ParentGenesis( 168 | tokenName: string, tokenTicker: string, tokenAmount: BigNumber, 169 | documentUri: string|null, documentHash: Buffer|null, tokenReceiverAddress: string, 170 | batonReceiverAddress: string, bchChangeReceiverAddress: string, 171 | inputUtxos: SlpAddressUtxoResult[], decimals= 0) { 172 | const genesisTxHex = this.txnHelpers.simpleNFT1ParentGenesis({ 173 | tokenName, tokenTicker, tokenAmount, documentUri, documentHash, 174 | tokenReceiverAddress, batonReceiverAddress, bchChangeReceiverAddress, inputUtxos, decimals, 175 | }); 176 | return this.sendTx(genesisTxHex); 177 | } 178 | 179 | public async simpleNFT1ChildGenesis( 180 | nft1GroupId: string, tokenName: string, tokenTicker: string, documentUri: string|null, 181 | documentHash: Buffer|null, tokenReceiverAddress: string, bchChangeReceiverAddress: string, 182 | inputUtxos: SlpAddressUtxoResult[], allowBurnAnyAmount = false) { 183 | const genesisTxHex = this.txnHelpers.simpleNFT1ChildGenesis({ 184 | nft1GroupId, tokenName, tokenTicker, documentUri, documentHash, tokenReceiverAddress, 185 | bchChangeReceiverAddress, inputUtxos, allowBurnAnyAmount, 186 | }); 187 | 188 | // include explicit burn allowance for burning Group token 189 | const burn: SlpRequiredBurn = { 190 | tokenId: Buffer.from(nft1GroupId, "hex"), 191 | tokenType: 129, 192 | amount: allowBurnAnyAmount ? inputUtxos[0].slpUtxoJudgementAmount.toFixed() : "1", 193 | outpointHash: Buffer.from(Buffer.from(inputUtxos[0].txid, "hex").reverse()), 194 | outpointVout: inputUtxos[0].vout 195 | }; 196 | 197 | return this.sendTx(genesisTxHex, [burn]); 198 | } 199 | 200 | // Sent SLP tokens to a single output address with change handled 201 | // (Warning: Sweeps all BCH/SLP UTXOs for the funding address) 202 | public async simpleTokenMint( 203 | tokenId: string, mintAmount: BigNumber, inputUtxos: SlpAddressUtxoResult[], 204 | tokenReceiverAddress: string, batonReceiverAddress: string, changeReceiverAddress: string) { 205 | const txHex = this.txnHelpers.simpleTokenMint({ 206 | tokenId, mintAmount, inputUtxos, tokenReceiverAddress, batonReceiverAddress, changeReceiverAddress, 207 | }); 208 | return this.sendTx(txHex); 209 | } 210 | 211 | // Burn a precise quantity of SLP tokens with remaining tokens (change) sent to a 212 | // single output address (Warning: Sweeps all BCH/SLP UTXOs for the funding address) 213 | public async simpleTokenBurn( 214 | tokenId: string, burnAmount: BigNumber, inputUtxos: SlpAddressUtxoResult[], changeReceiverAddress: string) { 215 | const txHex = this.txnHelpers.simpleTokenBurn({ 216 | tokenId, burnAmount, inputUtxos, changeReceiverAddress, 217 | }); 218 | return this.sendTx(txHex); 219 | } 220 | 221 | public async getUtxoWithRetry(address: string, retries = 40): Promise { 222 | return this.getUtxos(address); 223 | } 224 | 225 | public async getUtxoWithTxDetails(address: string): Promise { 226 | const res = await this.getUtxos(address); 227 | let utxos = Utils.mapToSlpAddressUtxoResultArray(res); 228 | const txIds = utxos.map((i: { txid: string; }) => i.txid); 229 | if (txIds.length === 0) { 230 | return []; 231 | } 232 | // Split txIds into chunks of 20 (BitBox limit), run the detail queries in parallel 233 | let txDetails: any[] = (await Promise.all(_.chunk(txIds, 20).map((txids: any[]) => { 234 | return this.getTransactionDetailsWithRetry([...new Set(txids)]); 235 | }))); 236 | // concat the chunked arrays 237 | txDetails = ([].concat(...txDetails) as TxnDetailsResult[]); 238 | utxos = utxos.map((i: SlpAddressUtxoResult) => { i.tx = txDetails.find((d: TxnDetailsResult) => d.txid === i.txid ); return i; }); 239 | return utxos; 240 | } 241 | 242 | public async getTransactionDetailsWithRetry(txids: string[], retries = 40): 243 | Promise { 244 | const results: Transaction[] = []; 245 | let count = 0; 246 | while (results.length !== txids.length) { 247 | for (const txid of txids) { 248 | const res = (await this.client 249 | .getTransaction({hash: txid, reversedHashOrder: true})) 250 | .getTransaction(); 251 | if (res) { 252 | results.push(res); 253 | } 254 | } 255 | if (results.length === txids.length) { 256 | let txns: TxnDetailsResult[]; 257 | txns = results.map((res: Transaction) => { 258 | return { 259 | txid: Buffer.from(res.getHash_asU8().reverse()).toString("hex"), 260 | version: res.getVersion(), 261 | locktime: res.getLockTime(), 262 | vin: res.getInputsList().map((i: { getIndex: () => any; getSequence: () => any; }) => { 263 | return { 264 | n: i.getIndex(), 265 | sequence: i.getSequence(), 266 | coinbase: null, 267 | }; }), 268 | vout: res.getOutputsList().map((o: { getValue: () => any; getIndex: () => any; getPubkeyScript_asU8: () => ArrayBuffer | SharedArrayBuffer; getDisassembledScript: () => any; }) => { 269 | return { 270 | value: o.getValue(), 271 | n: o.getIndex(), 272 | scriptPubKey: { 273 | hex: Buffer.from(o.getPubkeyScript_asU8()).toString("hex"), 274 | asm: o.getDisassembledScript(), 275 | }, 276 | }; }), 277 | time: res.getTimestamp(), 278 | blockhash: Buffer.from(res.getBlockHash_asU8().reverse()).toString("hex"), 279 | blockheight: res.getBlockHeight(), 280 | isCoinBase: false, 281 | valueOut: res.getOutputsList().reduce((p: any, o: { getValue: () => any; }) => p += o.getValue(), 0), 282 | size: res.getSize(), 283 | } as TxnDetailsResult; 284 | }); 285 | 286 | for (const txn of txns) { 287 | // add slp address format to transaction details 288 | txn.vin.forEach((input: any) => { 289 | try { input.slpAddress = Utils.toSlpAddress(input.legacyAddress); } catch (_) {} 290 | }); 291 | txn.vout.forEach((output: any) => { 292 | try { output.scriptPubKey.slpAddrs = [ Utils.toSlpAddress(output.scriptPubKey.cashAddrs[0]) ]; } catch (_) {} 293 | }); 294 | // add token information to transaction details 295 | try { 296 | (txn as SlpTxnDetailsResult).tokenInfo = await this.getTokenInformation(txn.txid, true); 297 | } catch (_) { 298 | (txn as SlpTxnDetailsResult).tokenIsValid = false; 299 | continue; 300 | } 301 | (txn as SlpTxnDetailsResult).tokenIsValid = await this.isValidSlpTxid(txn.txid); 302 | 303 | // add tokenNftParentId if token is a NFT child 304 | if ((txn as SlpTxnDetailsResult).tokenIsValid && (txn as SlpTxnDetailsResult).tokenInfo.versionType === SlpVersionType.TokenVersionType1_NFT_Child) { 305 | (txn as SlpTxnDetailsResult).tokenNftParentId = await this.getNftParentId((txn as SlpTxnDetailsResult).tokenInfo.tokenIdHex); 306 | } 307 | } 308 | return txns as SlpTxnDetailsResult[]; 309 | } 310 | count++; 311 | if (count > retries) { 312 | throw new Error("gRPC client.getTransaction endpoint experienced a problem"); 313 | } 314 | await sleep(250); 315 | } 316 | } 317 | 318 | public async getAddressDetailsWithRetry(address: string, retries = 40) { 319 | // must be a cash or legacy addr 320 | if (!bchaddr.isCashAddress(address) && !bchaddr.isLegacyAddress(address)) { 321 | throw new Error("Not an a valid address format, must be cashAddr or Legacy address format."); 322 | } 323 | const utxos = (await this.client.getAddressUtxos({ address, includeMempool: false })).getOutputsList(); 324 | const balance = utxos.reduce((p: any, o: { getValue: () => any; }) => o.getValue(), 0); 325 | const utxosMempool = (await this.client.getAddressUtxos({ address, includeMempool: true })).getOutputsList(); 326 | const mempoolBalance = utxosMempool.reduce((p: any, o: { getValue: () => any; }) => o.getValue(), 0); 327 | return { 328 | balance, 329 | balanceSat: balance * 10 ** 8, 330 | totalReceived: null, 331 | totalReceivedSat: null, 332 | totalSent: null, 333 | totalSentSat: null, 334 | unconfirmedBalance: mempoolBalance - balance, 335 | unconfirmedBalanceSat: mempoolBalance * 10 ** 8 - balance * 10 ** 8, 336 | unconfirmedTxAppearances: null, 337 | txAppearances: null, 338 | transactions: null, 339 | legacyAddress: bchaddr.toLegacyAddress(address), 340 | cashAddress: bchaddr.toCashAddress(address), 341 | slpAddress: bchaddr.toSlpAddress(address), 342 | } as any as AddressDetailsResult; 343 | } 344 | 345 | public async sendTx(hex: string, slpBurns?: SlpRequiredBurn[], suppressWarnings = false): Promise { 346 | let txn: SubmitTransactionResponse|undefined; 347 | try { 348 | txn = await this.client.submitTransaction({ 349 | txnHex: hex, 350 | requiredSlpBurns: slpBurns, 351 | }); 352 | } catch (err) { 353 | if (err.message.includes("BCHD instance does not have SLP indexing enabled")) { 354 | if (! suppressWarnings) { 355 | console.log(err.message); 356 | } 357 | } else { 358 | throw err; 359 | } 360 | } 361 | 362 | if (! txn) { 363 | txn = await this.client.submitTransaction({ 364 | txnHex: hex, 365 | skipSlpValidityChecks: true 366 | }); 367 | } 368 | 369 | return Buffer.from(txn.getHash_asU8().reverse()).toString("hex"); 370 | } 371 | 372 | public async monitorForPayment(paymentAddress: string, fee: number, onPaymentCB: Function): Promise { 373 | let utxo: AddressUtxoResult | undefined; 374 | // must be a cash or legacy addr 375 | if (!bchaddr.isCashAddress(paymentAddress) && !bchaddr.isLegacyAddress(paymentAddress)) { 376 | throw new Error("Not an a valid address format, must be cashAddr or Legacy address format."); 377 | } 378 | while (true) { 379 | try { 380 | utxo = await this.getUtxos(paymentAddress); 381 | if (utxo && utxo.utxos[0].satoshis >= fee) { 382 | break; 383 | } 384 | } catch (ex) { 385 | console.log(ex); 386 | } 387 | await sleep(2000); 388 | } 389 | onPaymentCB(); 390 | } 391 | 392 | public async processUtxosForSlp(utxos: SlpAddressUtxoResult[]): Promise { 393 | return this.slp.processUtxosForSlpAbstract(utxos, this); 394 | } 395 | 396 | public async getRawTransactions(txids: string[]): Promise { 397 | if (this.validator && this.validator.getRawTransactions) { 398 | return this.validator.getRawTransactions(txids); 399 | } 400 | const getTxnHex = async (txid: string) => { 401 | return Buffer.from((await this.client 402 | .getRawTransaction({ hash: txid, reversedHashOrder: true })) 403 | .getTransaction_asU8()).toString("hex"); 404 | }; 405 | return Promise.all(txids.map((txid) => getTxnHex(txid))); 406 | } 407 | 408 | public async isValidSlpTxid(txid: string): Promise { 409 | return this.validator.isValidSlpTxid(txid, null, null, this.logger); 410 | } 411 | 412 | public async validateSlpTransactions(txids: string[]): Promise { 413 | return this.validator.validateSlpTransactions(txids); 414 | } 415 | 416 | public async getNftParentId(tokenIdHex: string) { 417 | const txnhex = (await this.getRawTransactions([tokenIdHex]))[0]; 418 | const tx = Primatives.Transaction.parseFromBuffer(Buffer.from(txnhex, "hex")); 419 | const nftBurnTxnHex = (await this.getRawTransactions([tx.inputs[0].previousTxHash]))[0]; 420 | const nftBurnTxn = Primatives.Transaction.parseFromBuffer(Buffer.from(nftBurnTxnHex, "hex")); 421 | const slp = new Slp(this.BITBOX); 422 | const nftBurnSlp = slp.parseSlpOutputScript(Buffer.from(nftBurnTxn.outputs[0].scriptPubKey)); 423 | if (nftBurnSlp.transactionType === SlpTransactionType.GENESIS) { 424 | return tx.inputs[0].previousTxHash; 425 | } else { 426 | return nftBurnSlp.tokenIdHex; 427 | } 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /lib/bchdtrustedvalidator.ts: -------------------------------------------------------------------------------- 1 | import { IGrpcClient } from "grpc-bchrpc"; 2 | import { logger, SlpValidator } from ".."; 3 | 4 | export class BchdValidator implements SlpValidator { 5 | public client: IGrpcClient; 6 | private logger?: { log: (s: string) => any; }; 7 | constructor(client: IGrpcClient, logger?: logger) { 8 | this.client = client; 9 | if (logger) { 10 | this.logger = logger; 11 | } 12 | } 13 | public async getRawTransactions(txid: string[]): Promise { 14 | const res = await this.client.getRawTransaction({hash: txid[0], reversedHashOrder: true}); 15 | return [Buffer.from(res.getTransaction_asU8()).toString("hex")]; 16 | } 17 | public async isValidSlpTxid(txid: string): Promise { 18 | try { 19 | this.log(`validate: ${txid}`); 20 | const res = await this.client.getTrustedSlpValidation({ 21 | txos: [{vout: 1, hash: txid}], 22 | reversedHashOrder: true 23 | }); 24 | } catch (error) { 25 | if (! error.message.includes("txid is missing from slp validity set")) { 26 | throw error; 27 | } 28 | this.log(`false (${txid})`); 29 | return false; 30 | } 31 | this.log(`true (${txid})`); 32 | return true; 33 | } 34 | 35 | public async validateSlpTransactions(txids: string[]): Promise { 36 | const res = []; 37 | for (const txid of txids) { 38 | res.push((await this.isValidSlpTxid(txid)) ? txid : ""); 39 | } 40 | return res.filter((id: string) => id.length > 0); 41 | } 42 | 43 | private log(s: string) { 44 | if (this.logger) { 45 | this.logger.log(s); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/bitboxnetwork.ts: -------------------------------------------------------------------------------- 1 | import Axios from "axios"; 2 | import * as bchaddr from "bchaddrjs-slp"; 3 | import BigNumber from "bignumber.js"; 4 | import { BITBOX } from "bitbox-sdk"; 5 | import { AddressDetailsResult, AddressUtxoResult, 6 | TxnDetailsResult } from "bitcoin-com-rest"; 7 | import * as Bitcore from "bitcore-lib-cash"; 8 | import * as _ from "lodash"; 9 | import { INetwork, logger, Primatives, 10 | SlpAddressUtxoResult, SlpBalancesResult, SlpTransactionDetails, 11 | SlpTransactionType, SlpVersionType } from "../index"; 12 | import { Slp, SlpValidator } from "./slp"; 13 | import { TransactionHelpers } from "./transactionhelpers"; 14 | import { Utils } from "./utils"; 15 | 16 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 17 | 18 | export class BitboxNetwork implements INetwork { 19 | public BITBOX: BITBOX; 20 | public slp: Slp; 21 | public validator?: SlpValidator; 22 | public txnHelpers: TransactionHelpers; 23 | public logger: logger = { log: (s: string) => null }; 24 | 25 | constructor(BITBOX: BITBOX, validator?: SlpValidator, logger?: logger) { 26 | if (!BITBOX) { 27 | throw Error("Must provide BITBOX instance to class constructor."); 28 | } 29 | if (logger) { 30 | this.logger = logger; 31 | } 32 | if (validator) { 33 | this.validator = validator; 34 | } 35 | this.BITBOX = BITBOX; 36 | this.slp = new Slp(BITBOX); 37 | this.txnHelpers = new TransactionHelpers(this.slp); 38 | } 39 | 40 | public async getTokenInformation(txid: string, decimalConversion= false): Promise { 41 | let res: string[]; 42 | try { 43 | res = (await this.BITBOX.RawTransactions.getRawTransaction([txid]) as string[]|any); 44 | } catch (e) { 45 | throw Error(e.error); 46 | } 47 | if (!Array.isArray(res) || res.length !== 1) { 48 | throw Error("BITBOX response error for 'RawTransactions.getRawTransaction'"); 49 | } 50 | const txhex = res[0]; 51 | const txn: Bitcore.Transaction = new Bitcore.Transaction(txhex); 52 | const slpMsg = this.slp.parseSlpOutputScript(txn.outputs[0]._scriptBuffer); 53 | if (decimalConversion) { 54 | if ([SlpTransactionType.GENESIS, SlpTransactionType.MINT].includes(slpMsg.transactionType)) { 55 | slpMsg.genesisOrMintQuantity = slpMsg.genesisOrMintQuantity!.dividedBy(10 ** slpMsg.decimals); 56 | } else { 57 | slpMsg.sendOutputs!.map((o) => o.dividedBy(10 ** slpMsg.decimals)); 58 | } 59 | } 60 | if (SlpTransactionType.GENESIS === slpMsg.transactionType) { 61 | slpMsg.tokenIdHex = txid; 62 | } 63 | return slpMsg; 64 | } 65 | 66 | // WARNING: this method is limited to 60 transactions per minute 67 | public async getTransactionDetails(txid: string, decimalConversion = false) { 68 | const txn: any = (await this.BITBOX.Transaction.details([ txid ]) as TxnDetailsResult[])[0]; 69 | // add slp address format to transaction details 70 | txn.vin.forEach((input: any) => { 71 | try { input.slpAddress = Utils.toSlpAddress(input.legacyAddress); } catch (_) {} 72 | }); 73 | txn.vout.forEach((output: any) => { 74 | try { output.scriptPubKey.slpAddrs = [ Utils.toSlpAddress(output.scriptPubKey.cashAddrs[0]) ]; } catch (_) {} 75 | }); 76 | // add token information to transaction details 77 | txn.tokenInfo = await this.getTokenInformation(txid, decimalConversion); 78 | txn.tokenIsValid = this.validator ? 79 | await this.validator.isValidSlpTxid(txid, null, null, this.logger) : 80 | await this.isValidSlpTxid(txid); 81 | 82 | // add tokenNftParentId if token is a NFT child 83 | if (txn.tokenIsValid && txn.tokenInfo.versionType === SlpVersionType.TokenVersionType1_NFT_Child) { 84 | txn.tokenNftParentId = await this.getNftParentId(txn.tokenInfo.tokenIdHex); 85 | } 86 | 87 | return txn; 88 | } 89 | 90 | public async getUtxos(address: string) { 91 | // must be a cash or legacy addr 92 | let res: AddressUtxoResult; 93 | if (!bchaddr.isCashAddress(address) && !bchaddr.isLegacyAddress(address)) { 94 | throw new Error("Not an a valid address format, must be cashAddr or Legacy address format."); 95 | } 96 | res = (await this.BITBOX.Address.utxo([address]) as AddressUtxoResult[])[0]; 97 | return res; 98 | } 99 | 100 | public async getAllSlpBalancesAndUtxos(address: string|string[]): Promise> { 104 | if (typeof address === "string") { 105 | address = bchaddr.toCashAddress(address); 106 | const result = await this.getUtxoWithTxDetails(address); 107 | return await this.processUtxosForSlp(result); 108 | } 109 | address = address.map((a) => bchaddr.toCashAddress(a)); 110 | const results: Array<{ address: string, result: SlpBalancesResult }> = []; 111 | for (let i = 0; i < address.length; i++) { 112 | const utxos = await this.getUtxoWithTxDetails(address[i]); 113 | results.push({ address: Utils.toSlpAddress(address[i]), result: await this.processUtxosForSlp(utxos) }); 114 | } 115 | return results; 116 | } 117 | 118 | // Sent SLP tokens to a single output address with change handled 119 | // (Warning: Sweeps all BCH/SLP UTXOs for the funding address) 120 | public async simpleTokenSend( 121 | tokenId: string, sendAmounts: BigNumber|BigNumber[], inputUtxos: SlpAddressUtxoResult[], 122 | tokenReceiverAddresses: string|string[], changeReceiverAddress: string, requiredNonTokenOutputs 123 | : Array<{ satoshis: number, receiverAddress: string }> = []) { 124 | const txHex = this.txnHelpers.simpleTokenSend({ 125 | tokenId, sendAmounts, inputUtxos, tokenReceiverAddresses, 126 | changeReceiverAddress, requiredNonTokenOutputs, 127 | }); 128 | 129 | if (!inputUtxos.every((i) => typeof i.wif === "string")) { 130 | throw Error("The BitboxNetwork version of this method requires a private key WIF be provided with each input." + 131 | "If you want more control over the signing process use Slp.simpleTokenSend() to get the unsigned transaction," + 132 | "then after the transaction is signed you can use BitboxNetwork.sendTx()"); 133 | } 134 | 135 | return await this.sendTx(txHex); 136 | } 137 | 138 | public async simpleBchSend( 139 | sendAmounts: BigNumber|BigNumber[], inputUtxos: SlpAddressUtxoResult[], 140 | bchReceiverAddresses: string|string[], changeReceiverAddress: string) { 141 | const genesisTxHex = this.txnHelpers.simpleBchSend({ 142 | sendAmounts, inputUtxos, bchReceiverAddresses, changeReceiverAddress, 143 | }); 144 | return await this.sendTx(genesisTxHex); 145 | } 146 | 147 | public async simpleTokenGenesis( 148 | tokenName: string, tokenTicker: string, tokenAmount: BigNumber, documentUri: string|null, 149 | documentHash: Buffer|null, decimals: number, tokenReceiverAddress: string, batonReceiverAddress: string, 150 | bchChangeReceiverAddress: string, inputUtxos: SlpAddressUtxoResult[]) { 151 | const genesisTxHex = this.txnHelpers.simpleTokenGenesis({ 152 | tokenName, tokenTicker, tokenAmount, documentUri, documentHash, decimals, 153 | tokenReceiverAddress, batonReceiverAddress, bchChangeReceiverAddress, inputUtxos, 154 | }); 155 | return await this.sendTx(genesisTxHex); 156 | } 157 | 158 | public async simpleNFT1ParentGenesis( 159 | tokenName: string, tokenTicker: string, tokenAmount: BigNumber, 160 | documentUri: string|null, documentHash: Buffer|null, tokenReceiverAddress: string, 161 | batonReceiverAddress: string, bchChangeReceiverAddress: string, 162 | inputUtxos: SlpAddressUtxoResult[], decimals= 0) { 163 | const genesisTxHex = this.txnHelpers.simpleNFT1ParentGenesis({ 164 | tokenName, tokenTicker, tokenAmount, documentUri, documentHash, 165 | tokenReceiverAddress, batonReceiverAddress, bchChangeReceiverAddress, inputUtxos, decimals, 166 | }); 167 | return await this.sendTx(genesisTxHex); 168 | } 169 | 170 | public async simpleNFT1ChildGenesis( 171 | nft1GroupId: string, tokenName: string, tokenTicker: string, documentUri: string|null, 172 | documentHash: Buffer|null, tokenReceiverAddress: string, bchChangeReceiverAddress: string, 173 | inputUtxos: SlpAddressUtxoResult[], allowBurnAnyAmount= false) { 174 | const genesisTxHex = this.txnHelpers.simpleNFT1ChildGenesis({ 175 | nft1GroupId, tokenName, tokenTicker, documentUri, documentHash, tokenReceiverAddress, 176 | bchChangeReceiverAddress, inputUtxos, allowBurnAnyAmount, 177 | }); 178 | return await this.sendTx(genesisTxHex); 179 | } 180 | 181 | // Sent SLP tokens to a single output address with change handled 182 | // (Warning: Sweeps all BCH/SLP UTXOs for the funding address) 183 | public async simpleTokenMint( 184 | tokenId: string, mintAmount: BigNumber, inputUtxos: SlpAddressUtxoResult[], 185 | tokenReceiverAddress: string, batonReceiverAddress: string, changeReceiverAddress: string) { 186 | const txHex = this.txnHelpers.simpleTokenMint({ 187 | tokenId, mintAmount, inputUtxos, tokenReceiverAddress, batonReceiverAddress, changeReceiverAddress, 188 | }); 189 | return await this.sendTx(txHex); 190 | } 191 | 192 | // Burn a precise quantity of SLP tokens with remaining tokens (change) sent to a 193 | // single output address (Warning: Sweeps all BCH/SLP UTXOs for the funding address) 194 | public async simpleTokenBurn( 195 | tokenId: string, burnAmount: BigNumber, inputUtxos: SlpAddressUtxoResult[], changeReceiverAddress: string) { 196 | const txHex = this.txnHelpers.simpleTokenBurn({ 197 | tokenId, burnAmount, inputUtxos, changeReceiverAddress, 198 | }); 199 | return await this.sendTx(txHex); 200 | } 201 | 202 | public async getUtxoWithRetry(address: string, retries = 40) { 203 | let result: AddressUtxoResult | undefined; 204 | let count = 0; 205 | while (result === undefined) { 206 | result = await this.getUtxos(address); 207 | count++; 208 | if (count > retries) { 209 | throw new Error("this.BITBOX.Address.utxo endpoint experienced a problem"); 210 | } 211 | await sleep(250); 212 | } 213 | return result; 214 | } 215 | 216 | public async getUtxoWithTxDetails(address: string) { 217 | let utxos = Utils.mapToSlpAddressUtxoResultArray(await this.getUtxoWithRetry(address)); 218 | const txIds = utxos.map((i: { txid: string; }) => i.txid); 219 | if (txIds.length === 0) { 220 | return []; 221 | } 222 | // Split txIds into chunks of 20 (BitBox limit), run the detail queries in parallel 223 | let txDetails: any[] = (await Promise.all(_.chunk(txIds, 20).map((txids: any[]) => { 224 | return this.getTransactionDetailsWithRetry([...new Set(txids)]); 225 | }))); 226 | // concat the chunked arrays 227 | txDetails = ([].concat(...txDetails) as TxnDetailsResult[]); 228 | utxos = utxos.map((i: SlpAddressUtxoResult) => { i.tx = txDetails.find((d: TxnDetailsResult) => d.txid === i.txid ); return i; }); 229 | return utxos; 230 | } 231 | 232 | public async getTransactionDetailsWithRetry(txids: string[], retries = 40) { 233 | let result!: TxnDetailsResult[]; 234 | let count = 0; 235 | while (result === undefined) { 236 | result = (await this.BITBOX.Transaction.details(txids) as TxnDetailsResult[]); 237 | if (result) { 238 | return result; 239 | } 240 | count++; 241 | if (count > retries) { 242 | throw new Error("this.BITBOX.Address.details endpoint experienced a problem"); 243 | } 244 | await sleep(250); 245 | } 246 | } 247 | 248 | public async getAddressDetailsWithRetry(address: string, retries = 40) { 249 | // must be a cash or legacy addr 250 | if (!bchaddr.isCashAddress(address) && !bchaddr.isLegacyAddress(address)) { 251 | throw new Error("Not an a valid address format, must be cashAddr or Legacy address format."); 252 | } 253 | let result: AddressDetailsResult[] | undefined; 254 | let count = 0; 255 | while (result === undefined) { 256 | result = (await this.BITBOX.Address.details([address]) as AddressDetailsResult[]); 257 | if (result) { 258 | return result[0]; 259 | } 260 | count++; 261 | if (count > retries) { 262 | throw new Error("this.BITBOX.Address.details endpoint experienced a problem"); 263 | } 264 | await sleep(250); 265 | } 266 | } 267 | 268 | public async sendTx(hex: string): Promise { 269 | const res = await this.BITBOX.RawTransactions.sendRawTransaction([ hex ]as any); 270 | // console.log(res); 271 | if (typeof res === "object") { 272 | return (res as string[])[0]; 273 | } 274 | return res; 275 | } 276 | 277 | public async monitorForPayment(paymentAddress: string, fee: number, onPaymentCB: Function) { 278 | let utxo: AddressUtxoResult | undefined; 279 | // must be a cash or legacy addr 280 | if (!bchaddr.isCashAddress(paymentAddress) && !bchaddr.isLegacyAddress(paymentAddress)) { 281 | throw new Error("Not an a valid address format, must be cashAddr or Legacy address format."); 282 | } 283 | while (true) { 284 | try { 285 | utxo = await this.getUtxos(paymentAddress); 286 | if (utxo && utxo.utxos[0].satoshis >= fee) { 287 | break; 288 | } 289 | } catch (ex) { 290 | console.log(ex); 291 | } 292 | await sleep(2000); 293 | } 294 | onPaymentCB(); 295 | } 296 | 297 | public async getRawTransactions(txids: string[]): Promise { 298 | if (this.validator && this.validator.getRawTransactions) { 299 | return await this.validator.getRawTransactions(txids); 300 | } 301 | return await this.BITBOX.RawTransactions.getRawTransaction(txids) as any[]; 302 | } 303 | 304 | public async processUtxosForSlp(utxos: SlpAddressUtxoResult[]): Promise { 305 | return await this.slp.processUtxosForSlpAbstract(utxos, this); 306 | } 307 | 308 | public async isValidSlpTxid(txid: string): Promise { 309 | if (this.validator) { 310 | return await this.validator.isValidSlpTxid(txid, null, null, this.logger); 311 | } 312 | // WARNING: the following method is limited to 60 transactions per minute 313 | const validatorUrl = this.setRemoteValidatorUrl(); 314 | this.logger.log("SLPJS Validating (remote: " + validatorUrl + "): " + txid); 315 | const result = await Axios({ 316 | method: "post", 317 | url: validatorUrl, 318 | data: { 319 | txids: [ txid ], 320 | }, 321 | }); 322 | let res = false; 323 | if (result && result.data) { 324 | res = (result.data as Array<{ txid: string, valid: boolean }>).filter((i) => i.valid).length > 0 ? true : false; 325 | } 326 | this.logger.log("SLPJS Validator Result: " + res + " (remote: " + validatorUrl + "): " + txid); 327 | return res; 328 | } 329 | 330 | public async validateSlpTransactions(txids: string[]): Promise { 331 | if (this.validator) { 332 | return await this.validator.validateSlpTransactions(txids); 333 | } 334 | const validatorUrl = this.setRemoteValidatorUrl(); 335 | 336 | const promises = _.chunk(txids, 20).map((ids) => Axios({ 337 | method: "post", 338 | url: validatorUrl, 339 | data: { 340 | txids: ids, 341 | }, 342 | })); 343 | const results = await Axios.all(promises); 344 | const result = { data: [] }; 345 | results.forEach((res) => { 346 | if (res.data) { 347 | result.data = result.data.concat(res.data); 348 | } 349 | }); 350 | if (result && result.data) { 351 | return (result.data as Array<{ txid: string, valid: boolean }>) 352 | .filter((i) => i.valid).map((i) => i.txid); 353 | } 354 | return []; 355 | } 356 | 357 | public async getNftParentId(tokenIdHex: string) { 358 | const txnhex = (await this.getRawTransactions([tokenIdHex]))[0]; 359 | const tx = Primatives.Transaction.parseFromBuffer(Buffer.from(txnhex, "hex")); 360 | const nftBurnTxnHex = (await this.getRawTransactions([tx.inputs[0].previousTxHash]))[0]; 361 | const nftBurnTxn = Primatives.Transaction.parseFromBuffer(Buffer.from(nftBurnTxnHex, "hex")); 362 | const slp = new Slp(this.BITBOX); 363 | const nftBurnSlp = slp.parseSlpOutputScript(Buffer.from(nftBurnTxn.outputs[0].scriptPubKey)); 364 | if (nftBurnSlp.transactionType === SlpTransactionType.GENESIS) { 365 | return tx.inputs[0].previousTxHash; 366 | } else { 367 | return nftBurnSlp.tokenIdHex; 368 | } 369 | } 370 | 371 | private setRemoteValidatorUrl() { 372 | let validatorUrl = this.BITBOX.restURL.replace("v1", "v2"); 373 | validatorUrl = validatorUrl.concat("/slp/validateTxid"); 374 | validatorUrl = validatorUrl.replace("//slp", "/slp"); 375 | return validatorUrl; 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /lib/bitdbnetwork.ts: -------------------------------------------------------------------------------- 1 | import { SlpTransactionDetails, SlpTransactionType } from '../index'; 2 | 3 | import axios, { AxiosRequestConfig } from 'axios'; 4 | import BigNumber from 'bignumber.js'; 5 | 6 | export class BitdbNetwork { 7 | bitdbUrl: string; 8 | 9 | constructor(bitdbUrl='https://bitdb.bch.sx/q/'){ 10 | this.bitdbUrl = bitdbUrl; 11 | } 12 | 13 | async getTokenInformation(tokenId: string): Promise { 14 | 15 | let query = { 16 | "v": 3, 17 | "q": { 18 | "find": { "out.h1": "534c5000", "out.s3": "GENESIS", "tx.h": tokenId } 19 | }, 20 | "r": { "f": "[ .[] | { token_type: .out[0].h2, timestamp: (if .blk? then (.blk.t | strftime(\"%Y-%m-%d %H:%M\")) else null end), symbol: .out[0].s4, name: .out[0].s5, document: .out[0].s6, document_sha256: .out[0].h7, decimals: .out[0].h8, baton: .out[0].h9, quantity: .out[0].h10, URI: \"https://tokengraph.network/token/\\(.tx.h)\" } ]" } 21 | } 22 | 23 | const data = Buffer.from(JSON.stringify(query)).toString('base64'); 24 | 25 | let config: AxiosRequestConfig = { 26 | method: 'GET', 27 | url: this.bitdbUrl + data 28 | }; 29 | 30 | const response = (await axios(config)).data; 31 | 32 | const list = []; 33 | if(response.c){ 34 | list.push(...response.c); 35 | } 36 | if(response.u){ 37 | list.push(...response.u); 38 | } 39 | if(list.length === 0) { 40 | throw new Error('Token not found'); 41 | } 42 | 43 | let tokenDetails: SlpTransactionDetails = { 44 | transactionType: SlpTransactionType.GENESIS, 45 | tokenIdHex: tokenId, 46 | versionType: parseInt(list[0].token_type, 16), 47 | timestamp: list[0].timestamp, 48 | symbol: list[0].symbol, 49 | name: list[0].name, 50 | documentUri: list[0].document, 51 | documentSha256: list[0].document_sha256 ? Buffer.from(list[0].document_sha256) : null, 52 | decimals: parseInt(list[0].decimals, 16) || 0, 53 | containsBaton: Buffer.from(list[0].baton,'hex').readUIntBE(0,1) >= 2, 54 | batonVout: Buffer.from(list[0].baton,'hex').readUIntBE(0,1), 55 | genesisOrMintQuantity: new BigNumber(list[0].quantity, 16).dividedBy(10**(parseInt(list[0].decimals, 16))) 56 | } 57 | 58 | return tokenDetails; 59 | } 60 | } -------------------------------------------------------------------------------- /lib/crypto.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from "crypto-js"; 2 | 3 | export class Crypto { 4 | public static sha256(buffer: Buffer) { 5 | const words = CryptoJS.enc.Hex.parse(buffer.toString("hex")); 6 | const hash = CryptoJS.SHA256(words); 7 | return Buffer.from(hash.toString(CryptoJS.enc.Hex), "hex"); 8 | } 9 | public static hash256(buffer: Buffer) { 10 | return this.sha256(this.sha256(buffer)); 11 | } 12 | public static txid(buffer: Buffer) { 13 | return Buffer.from(this.hash256(buffer).reverse()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/localvalidator.ts: -------------------------------------------------------------------------------- 1 | import { logger, SlpTransactionDetails, SlpTransactionType } from "../index"; 2 | import { Slp, SlpValidator } from "./slp"; 3 | 4 | import BigNumber from "bignumber.js"; 5 | import { BITBOX } from "bitbox-sdk"; 6 | import * as Bitcore from "bitcore-lib-cash"; 7 | 8 | import { Crypto } from "./crypto"; 9 | 10 | export interface Validation { validity: boolean|null; parents: Parent[]; details: SlpTransactionDetails|null; invalidReason: string|null; waiting: boolean; } 11 | export type GetRawTransactionsAsync = (txid: string[]) => Promise; 12 | 13 | const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 14 | 15 | interface Parent { 16 | txid: string; 17 | vout: number; 18 | versionType: number; 19 | valid: boolean|null; 20 | inputQty: BigNumber|null; 21 | } 22 | 23 | export class LocalValidator implements SlpValidator { 24 | public BITBOX: BITBOX; 25 | public cachedRawTransactions: { [txid: string]: string }; 26 | public useTransactionCache: boolean; 27 | public cachedValidations: { [txid: string]: Validation }; 28 | public getRawTransactions: GetRawTransactionsAsync; 29 | public slp: Slp; 30 | public logger: logger = { log: (s: string) => null }; 31 | 32 | constructor(BITBOX: BITBOX, getRawTransactions: GetRawTransactionsAsync, logger?: logger, useTransactionCache = true) { 33 | if (!BITBOX) { 34 | throw Error("Must provide BITBOX instance to class constructor."); 35 | } 36 | if (!getRawTransactions) { 37 | throw Error("Must provide method getRawTransactions to class constructor."); 38 | } 39 | if (logger) { 40 | this.logger = logger; 41 | } 42 | this.BITBOX = BITBOX; 43 | this.getRawTransactions = getRawTransactions; 44 | this.slp = new Slp(BITBOX); 45 | this.cachedValidations = {}; 46 | this.cachedRawTransactions = {}; 47 | this.useTransactionCache = useTransactionCache; 48 | } 49 | 50 | public addValidationFromStore(hex: string, isValid: boolean) { 51 | const id = Crypto.txid(Buffer.from(hex, "hex")).toString("hex"); 52 | if (!this.cachedValidations[id]) { 53 | this.cachedValidations[id] = { validity: isValid, parents: [], details: null, invalidReason: null, waiting: false }; 54 | } 55 | if (!this.cachedRawTransactions[id] && this.useTransactionCache) { 56 | this.cachedRawTransactions[id] = hex; 57 | } 58 | } 59 | 60 | public async waitForCurrentValidationProcessing(txid: string) { 61 | const cached: Validation = this.cachedValidations[txid]; 62 | 63 | while (true) { 64 | if (typeof cached.validity === "boolean") { 65 | cached.waiting = false; 66 | break; 67 | } 68 | await sleep(10); 69 | } 70 | } 71 | 72 | public async waitForTransactionDownloadToComplete(txid: string) { 73 | while (true) { 74 | if (this.cachedRawTransactions[txid] && this.cachedRawTransactions[txid] !== "waiting") { 75 | break; 76 | } 77 | await sleep(10); 78 | } 79 | } 80 | 81 | public async retrieveRawTransaction(txid: string) { 82 | if (this.useTransactionCache && !this.cachedRawTransactions[txid]) { 83 | this.cachedRawTransactions[txid] = "waiting"; 84 | const txns = await this.getRawTransactions([txid]); 85 | if (!txns || txns.length === 0 || typeof txns[0] !== "string") { 86 | throw Error(`Response error in getRawTransactions, got: ${txns}`); 87 | } 88 | this.cachedRawTransactions[txid] = txns[0]; 89 | return txns[0]; 90 | } else if (this.useTransactionCache) { 91 | return this.cachedRawTransactions[txid]; 92 | } else { 93 | return await this.getRawTransactions([txid]); 94 | } 95 | } 96 | 97 | public async isValidSlpTxid(txid: string, tokenIdFilter?: string, tokenTypeFilter?: number): Promise { 98 | this.logger.log("SLPJS Validating: " + txid); 99 | const valid = await this._isValidSlpTxid(txid, tokenIdFilter, tokenTypeFilter); 100 | this.logger.log("SLPJS Result: " + valid + " (" + txid + ")"); 101 | if (!valid && this.cachedValidations[txid].invalidReason) { 102 | this.logger.log("SLPJS Invalid Reason: " + this.cachedValidations[txid].invalidReason); 103 | } else if (!valid) { 104 | this.logger.log("SLPJS Invalid Reason: unknown (result is user supplied)"); 105 | } 106 | return valid; 107 | } 108 | 109 | // 110 | // This method uses recursion to do a Depth-First-Search with the node result being 111 | // computed in Postorder Traversal (left/right/root) order. A validation cache 112 | // is used to keep track of the results for nodes that have already been evaluated. 113 | // 114 | // Each call to this method evaluates node validity with respect to 115 | // its parent node(s), so it walks backwards until the 116 | // validation cache provides a result or the GENESIS node is evaluated. 117 | // Root nodes await the validation result of their upstream parent. 118 | // 119 | // In the case of NFT1 the search continues to the group/parent NFT DAG after the Genesis 120 | // of the NFT child is discovered. 121 | // 122 | public async _isValidSlpTxid(txid: string, tokenIdFilter?: string, tokenTypeFilter?: number): Promise { 123 | // Check to see if this txn has been processed by looking at shared cache, if doesn't exist then download txn. 124 | if (!this.cachedValidations[txid]) { 125 | this.cachedValidations[txid] = { 126 | validity: null, 127 | parents: [], 128 | details: null, 129 | invalidReason: null, 130 | waiting: false, 131 | }; 132 | await this.retrieveRawTransaction(txid); 133 | } 134 | // Otherwise, we can use the cached result as long as a special filter isn't being applied. 135 | else if (typeof this.cachedValidations[txid].validity === "boolean") { 136 | return this.cachedValidations[txid].validity!; 137 | } 138 | 139 | // 140 | // Handle the case where neither branch of the previous if/else statement was 141 | // executed and the raw transaction has never been downloaded. 142 | // 143 | // Also handle case where a 2nd request of same txid comes in 144 | // during the download of a previous request. 145 | // 146 | if (!this.cachedRawTransactions[txid] || this.cachedRawTransactions[txid] === "waiting") { 147 | if (this.useTransactionCache) { 148 | if (this.cachedRawTransactions[txid] !== "waiting") { 149 | this.retrieveRawTransaction(txid); 150 | } 151 | 152 | // Wait for previously a initiated download to completed 153 | await this.waitForTransactionDownloadToComplete(txid); 154 | } else { 155 | await this.retrieveRawTransaction(txid); 156 | } 157 | } 158 | 159 | // Handle case where txid is already in the process of being validated from a previous call 160 | if (this.cachedValidations[txid].waiting) { 161 | await this.waitForCurrentValidationProcessing(txid); 162 | if (typeof this.cachedValidations[txid].validity === "boolean") { 163 | return this.cachedValidations[txid].validity!; 164 | } 165 | } 166 | 167 | this.cachedValidations[txid].waiting = true; 168 | 169 | // Check SLP message validity 170 | const txn: Bitcore.Transaction = new Bitcore.Transaction(this.cachedRawTransactions[txid]); 171 | let slpmsg: SlpTransactionDetails; 172 | try { 173 | slpmsg = this.cachedValidations[txid].details = this.slp.parseSlpOutputScript(txn.outputs[0]._scriptBuffer); 174 | if (slpmsg.transactionType === SlpTransactionType.GENESIS) { 175 | slpmsg.tokenIdHex = txid; 176 | } 177 | } catch (e) { 178 | this.cachedValidations[txid].validity = false; 179 | this.cachedValidations[txid].waiting = false; 180 | this.cachedValidations[txid].invalidReason = "SLP OP_RETURN parsing error (" + e.message + ")."; 181 | return this.cachedValidations[txid].validity!; 182 | } 183 | 184 | // Check for tokenId filter 185 | if (tokenIdFilter && slpmsg.tokenIdHex !== tokenIdFilter) { 186 | this.cachedValidations[txid].waiting = false; 187 | this.cachedValidations[txid].invalidReason = "Validator was run with filter only considering tokenId " + tokenIdFilter + " as valid."; 188 | return false; // Don't save boolean result to cache incase cache is ever used without tokenIdFilter. 189 | } else { 190 | if (this.cachedValidations[txid].validity !== false) { 191 | this.cachedValidations[txid].invalidReason = null; 192 | } 193 | } 194 | 195 | // Check specified token type is being respected 196 | if (tokenTypeFilter && slpmsg.versionType !== tokenTypeFilter) { 197 | this.cachedValidations[txid].validity = null; 198 | this.cachedValidations[txid].waiting = false; 199 | this.cachedValidations[txid].invalidReason = "Validator was run with filter only considering token type: " + tokenTypeFilter + " as valid."; 200 | return false; // Don't save boolean result to cache incase cache is ever used with different token type. 201 | } else { 202 | if (this.cachedValidations[txid].validity !== false) { 203 | this.cachedValidations[txid].invalidReason = null; 204 | } 205 | } 206 | 207 | // Check DAG validity 208 | if (slpmsg.transactionType === SlpTransactionType.GENESIS) { 209 | // Check for NFT1 child (type 0x41) 210 | if (slpmsg.versionType === 0x41) { 211 | // An NFT1 parent should be provided at input index 0, 212 | // so we check this first before checking the whole parent DAG 213 | const input_txid = txn.inputs[0].prevTxId.toString("hex"); 214 | const input_prevout = txn.inputs[0].outputIndex; 215 | const input_txhex = await this.retrieveRawTransaction(input_txid); 216 | const input_tx: Bitcore.Transaction = new Bitcore.Transaction(input_txhex); 217 | let input_slpmsg; 218 | try { 219 | input_slpmsg = this.slp.parseSlpOutputScript(input_tx.outputs[0]._scriptBuffer); 220 | } catch (_) { } 221 | if (!input_slpmsg || input_slpmsg.versionType !== 0x81) { 222 | this.cachedValidations[txid].validity = false; 223 | this.cachedValidations[txid].waiting = false; 224 | this.cachedValidations[txid].invalidReason = "NFT1 child GENESIS does not have a valid NFT1 parent input."; 225 | return this.cachedValidations[txid].validity!; 226 | } 227 | // Check that the there is a burned output >0 in the parent txn SLP message 228 | if (input_slpmsg.transactionType === SlpTransactionType.SEND) { 229 | if (input_prevout > input_slpmsg.sendOutputs!.length - 1) { 230 | this.cachedValidations[txid].validity = false; 231 | this.cachedValidations[txid].waiting = false; 232 | this.cachedValidations[txid].invalidReason = "NFT1 child GENESIS does not have a valid NFT1 parent input."; 233 | return this.cachedValidations[txid].validity!; 234 | } else if (! input_slpmsg.sendOutputs![input_prevout].isGreaterThan(0)) { 235 | this.cachedValidations[txid].validity = false; 236 | this.cachedValidations[txid].waiting = false; 237 | this.cachedValidations[txid].invalidReason = "NFT1 child's parent has SLP output that is not greater than zero."; 238 | return this.cachedValidations[txid].validity!; 239 | } else { 240 | this.cachedValidations[txid].validity = true; 241 | this.cachedValidations[txid].waiting = false; 242 | } 243 | } else if (input_slpmsg.transactionType === SlpTransactionType.GENESIS || 244 | input_slpmsg.transactionType === SlpTransactionType.MINT) { 245 | if (input_prevout !== 1) { 246 | this.cachedValidations[txid].validity = false; 247 | this.cachedValidations[txid].waiting = false; 248 | this.cachedValidations[txid].invalidReason = "NFT1 child GENESIS does not have a valid NFT1 parent input."; 249 | return this.cachedValidations[txid].validity!; 250 | } else if (!input_slpmsg.genesisOrMintQuantity!.isGreaterThan(0)) { 251 | this.cachedValidations[txid].validity = false; 252 | this.cachedValidations[txid].waiting = false; 253 | this.cachedValidations[txid].invalidReason = "NFT1 child's parent has SLP output that is not greater than zero."; 254 | return this.cachedValidations[txid].validity!; 255 | } 256 | } 257 | // Continue to check the NFT1 parent DAG 258 | let nft_parent_dag_validity = await this.isValidSlpTxid(input_txid, undefined, 0x81); 259 | this.cachedValidations[txid].validity = nft_parent_dag_validity; 260 | this.cachedValidations[txid].waiting = false; 261 | if (!nft_parent_dag_validity) { 262 | this.cachedValidations[txid].invalidReason = "NFT1 child GENESIS does not have valid parent DAG."; 263 | } 264 | return this.cachedValidations[txid].validity!; 265 | } 266 | // All other supported token types (includes 0x01 and 0x81) 267 | // No need to check type here since op_return parser throws on other types. 268 | else { 269 | this.cachedValidations[txid].validity = true; 270 | this.cachedValidations[txid].waiting = false; 271 | return this.cachedValidations[txid].validity!; 272 | } 273 | } 274 | else if (slpmsg.transactionType === SlpTransactionType.MINT) { 275 | for (let i = 0; i < txn.inputs.length; i++) { 276 | let input_txid = txn.inputs[i].prevTxId.toString("hex"); 277 | let input_txhex = await this.retrieveRawTransaction(input_txid); 278 | let input_tx: Bitcore.Transaction = new Bitcore.Transaction(input_txhex); 279 | try { 280 | let input_slpmsg = this.slp.parseSlpOutputScript(input_tx.outputs[0]._scriptBuffer); 281 | if (input_slpmsg.transactionType === SlpTransactionType.GENESIS) { 282 | input_slpmsg.tokenIdHex = input_txid; 283 | } 284 | if (input_slpmsg.tokenIdHex === slpmsg.tokenIdHex) { 285 | if (input_slpmsg.transactionType === SlpTransactionType.GENESIS || input_slpmsg.transactionType === SlpTransactionType.MINT) { 286 | if (txn.inputs[i].outputIndex === input_slpmsg.batonVout) { 287 | this.cachedValidations[txid].parents.push({ 288 | txid: txn.inputs[i].prevTxId.toString("hex"), 289 | vout: txn.inputs[i].outputIndex!, 290 | versionType: input_slpmsg.versionType, 291 | valid: null, 292 | inputQty: null, 293 | }); 294 | } 295 | } 296 | } 297 | } catch (_) {} 298 | } 299 | if (this.cachedValidations[txid].parents.length < 1) { 300 | this.cachedValidations[txid].validity = false; 301 | this.cachedValidations[txid].waiting = false; 302 | this.cachedValidations[txid].invalidReason = "MINT transaction must have at least 1 candidate baton parent input."; 303 | return this.cachedValidations[txid].validity!; 304 | } 305 | } 306 | else if (slpmsg.transactionType === SlpTransactionType.SEND) { 307 | const tokenOutQty = slpmsg.sendOutputs!.reduce((t, v) => { return t.plus(v); }, new BigNumber(0)); 308 | let tokenInQty = new BigNumber(0); 309 | for (let i = 0; i < txn.inputs.length; i++) { 310 | let input_txid = txn.inputs[i].prevTxId.toString("hex"); 311 | let input_txhex = await this.retrieveRawTransaction(input_txid); 312 | let input_tx: Bitcore.Transaction = new Bitcore.Transaction(input_txhex); 313 | try { 314 | let input_slpmsg = this.slp.parseSlpOutputScript(input_tx.outputs[0]._scriptBuffer); 315 | if (input_slpmsg.transactionType === SlpTransactionType.GENESIS) { 316 | input_slpmsg.tokenIdHex = input_txid; 317 | } 318 | if (input_slpmsg.tokenIdHex === slpmsg.tokenIdHex) { 319 | if (input_slpmsg.transactionType === SlpTransactionType.SEND) { 320 | if (txn.inputs[i].outputIndex! <= input_slpmsg.sendOutputs!.length - 1) { 321 | tokenInQty = tokenInQty.plus(input_slpmsg.sendOutputs![txn.inputs[i].outputIndex!]); 322 | this.cachedValidations[txid].parents.push({ 323 | txid: txn.inputs[i].prevTxId.toString("hex"), 324 | vout: txn.inputs[i].outputIndex!, 325 | versionType: input_slpmsg.versionType, 326 | valid: null, 327 | inputQty: input_slpmsg.sendOutputs![txn.inputs[i].outputIndex!] 328 | }); 329 | } 330 | } 331 | else if (input_slpmsg.transactionType === SlpTransactionType.GENESIS || input_slpmsg.transactionType === SlpTransactionType.MINT) { 332 | if (txn.inputs[i].outputIndex === 1) { 333 | tokenInQty = tokenInQty.plus(input_slpmsg.genesisOrMintQuantity!); 334 | this.cachedValidations[txid].parents.push({ 335 | txid: txn.inputs[i].prevTxId.toString("hex"), 336 | vout: txn.inputs[i].outputIndex!, 337 | versionType: input_slpmsg.versionType, 338 | valid: null, 339 | inputQty: input_slpmsg.genesisOrMintQuantity 340 | }); 341 | } 342 | } 343 | } 344 | } catch (_) {} 345 | } 346 | 347 | // Check token inputs are greater than token outputs (includes valid and invalid inputs) 348 | if (tokenOutQty.isGreaterThan(tokenInQty)) { 349 | this.cachedValidations[txid].validity = false; 350 | this.cachedValidations[txid].waiting = false; 351 | this.cachedValidations[txid].invalidReason = "Token outputs are greater than possible token inputs."; 352 | return this.cachedValidations[txid].validity!; 353 | } 354 | } 355 | 356 | // Set validity validation-cache for parents, and handle MINT condition with no valid input 357 | // we don't need to check proper token id since we only added parents with same ID in above steps. 358 | const parentTxids = [...new Set(this.cachedValidations[txid].parents.map(p => p.txid))]; 359 | for (const id of parentTxids) { 360 | const valid = await this.isValidSlpTxid(id); 361 | this.cachedValidations[txid].parents.filter(p => p.txid === id).map(p => p.valid = valid); 362 | } 363 | 364 | // Check MINT for exactly 1 valid MINT baton 365 | if (this.cachedValidations[txid].details!.transactionType === SlpTransactionType.MINT) { 366 | if (this.cachedValidations[txid].parents.filter(p => p.valid && p.inputQty === null).length !== 1) { 367 | this.cachedValidations[txid].validity = false; 368 | this.cachedValidations[txid].waiting = false; 369 | this.cachedValidations[txid].invalidReason = "MINT transaction with invalid baton parent."; 370 | return this.cachedValidations[txid].validity!; 371 | } 372 | } 373 | 374 | // Check valid inputs are greater than token outputs 375 | if (this.cachedValidations[txid].details!.transactionType === SlpTransactionType.SEND) { 376 | const validInputQty = this.cachedValidations[txid].parents.reduce((t, v) => { return v.valid ? t.plus(v.inputQty!) : t; }, new BigNumber(0)); 377 | const tokenOutQty = slpmsg.sendOutputs!.reduce((t, v) => { return t.plus(v); }, new BigNumber(0)); 378 | if (tokenOutQty.isGreaterThan(validInputQty)) { 379 | this.cachedValidations[txid].validity = false; 380 | this.cachedValidations[txid].waiting = false; 381 | this.cachedValidations[txid].invalidReason = "Token outputs are greater than valid token inputs."; 382 | return this.cachedValidations[txid].validity!; 383 | } 384 | } 385 | 386 | // Check versionType is not different from valid parents 387 | if (this.cachedValidations[txid].parents.filter(p => p.valid).length > 0) { 388 | const validVersionType = this.cachedValidations[txid].parents.find(p => p.valid!)!.versionType; 389 | if (this.cachedValidations[txid].details!.versionType !== validVersionType) { 390 | this.cachedValidations[txid].validity = false; 391 | this.cachedValidations[txid].waiting = false; 392 | this.cachedValidations[txid].invalidReason = "SLP version/type mismatch from valid parent."; 393 | return this.cachedValidations[txid].validity!; 394 | } 395 | } 396 | this.cachedValidations[txid].validity = true; 397 | this.cachedValidations[txid].waiting = false; 398 | return this.cachedValidations[txid].validity!; 399 | } 400 | 401 | public async validateSlpTransactions(txids: string[]): Promise { 402 | const res = []; 403 | for (let i = 0; i < txids.length; i++) { 404 | res.push((await this.isValidSlpTxid(txids[i])) ? txids[i] : ""); 405 | } 406 | return res.filter((id: string) => id.length > 0); 407 | } 408 | } -------------------------------------------------------------------------------- /lib/primatives.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line: no-namespace 2 | export namespace Primatives { 3 | class Hex { 4 | public static decode(text: string) { 5 | return text.match(/.{2}/g)!.map((byte) => { 6 | return parseInt(byte, 16); 7 | }); 8 | } 9 | public static encode(bytes: number[]) { 10 | const result = []; 11 | for (let i = 0, hex; i < bytes.length; i++) { 12 | hex = bytes[i].toString(16); 13 | if (hex.length < 2) { 14 | hex = "0" + hex; 15 | } 16 | result.push(hex); 17 | } 18 | return result.join(""); 19 | } 20 | } 21 | 22 | // tslint:disable-next-line: max-classes-per-file 23 | class LittleEndian { 24 | public static decode(bytes: number[]) { 25 | return bytes.reduce((previous, current, index) => { 26 | return previous + current * Math.pow(256, index); 27 | }, 0); 28 | } 29 | public static encode(number: number, count: number) { 30 | let rawBytes = []; 31 | for (let i = 0; i < count; i++) { 32 | rawBytes[i] = number & 0xff; 33 | number = Math.floor(number / 256); 34 | } 35 | return rawBytes; 36 | } 37 | } 38 | 39 | // tslint:disable-next-line: max-classes-per-file 40 | export class ArraySource { 41 | public rawBytes: number[]; 42 | public index: number; 43 | constructor(rawBytes: number[], index?: number) { 44 | this.rawBytes = rawBytes; 45 | this.index = index || 0; 46 | } 47 | public readByte() { 48 | if (!this.hasMoreBytes()) { 49 | throw new Error("Cannot read past the end of the array."); 50 | } 51 | return this.rawBytes[this.index++]; 52 | } 53 | public hasMoreBytes() { 54 | return this.index < this.rawBytes.length; 55 | } 56 | public getPosition() { 57 | return this.index; 58 | } 59 | } 60 | 61 | // tslint:disable-next-line: max-classes-per-file 62 | export class ByteStream { 63 | public source: ArraySource; 64 | constructor(source: ArraySource){ 65 | this.source = source; 66 | } 67 | public readByte() { 68 | return this.source.readByte(); 69 | } 70 | public readBytes(num: number) { 71 | var bytes = []; 72 | for (var i = 0; i < num; i++) { 73 | bytes.push(this.readByte()); 74 | } 75 | return bytes; 76 | } 77 | public readInt(num: number) { 78 | var bytes = this.readBytes(num); 79 | return LittleEndian.decode(bytes); 80 | } 81 | public readVarInt() { 82 | var num = this.readByte(); 83 | if (num < 0xfd) { 84 | return num; 85 | } else if (num === 0xfd) { 86 | return this.readInt(2); 87 | } else if (num === 0xfe) { 88 | return this.readInt(4); 89 | } else { 90 | return this.readInt(8); 91 | } 92 | } 93 | public readString() { 94 | var length = this.readVarInt(); 95 | return this.readBytes(length); 96 | } 97 | public readHexBytes(num: number) { 98 | var bytes = this.readBytes(num); 99 | return Hex.encode(bytes.reverse()); 100 | } 101 | public hasMoreBytes() { 102 | return this.source.hasMoreBytes(); 103 | } 104 | public getPosition() { 105 | return this.source.getPosition(); 106 | } 107 | } 108 | 109 | export interface TransactionInput { 110 | previousTxHash: string; 111 | previousTxOutIndex: number; 112 | scriptSig: number[]; 113 | sequenceNo: string; 114 | incomplete: boolean; 115 | satoshis?: number; 116 | } 117 | 118 | export interface TransactionOutput { 119 | scriptPubKey: number[]; 120 | value: number; 121 | } 122 | 123 | // tslint:disable-next-line: max-classes-per-file 124 | export class Transaction { 125 | public static parseFromBuffer(buffer: Buffer) { 126 | const source = new Primatives.ArraySource(buffer.toJSON().data); 127 | const stream = new Primatives.ByteStream(source); 128 | return Transaction.parse(stream); 129 | } 130 | 131 | public static parse(stream: ByteStream, mayIncludeUnsignedInputs = false) { 132 | const transaction = new Transaction(); 133 | transaction.version = stream.readInt(4); 134 | 135 | const txInNum = stream.readVarInt(); 136 | for (let i = 0; i < txInNum; i++) { 137 | const input: TransactionInput = { 138 | previousTxHash: stream.readHexBytes(32), 139 | previousTxOutIndex: stream.readInt(4), 140 | scriptSig: stream.readString(), 141 | sequenceNo: stream.readHexBytes(4), 142 | // tslint:disable-next-line: object-literal-sort-keys 143 | incomplete: false, 144 | }; 145 | 146 | if (mayIncludeUnsignedInputs && 147 | Buffer.from(input.scriptSig).toString("hex").includes("01ff")) { 148 | input.satoshis = stream.readInt(8); 149 | input.incomplete = true; 150 | } 151 | 152 | transaction.inputs.push(input); 153 | } 154 | 155 | let txOutNum = stream.readVarInt(); 156 | for (let i = 0; i < txOutNum; i++) { 157 | transaction.outputs.push({ 158 | value: stream.readInt(8), 159 | // tslint:disable-next-line: object-literal-sort-keys 160 | scriptPubKey: stream.readString(), 161 | }); 162 | } 163 | 164 | transaction.lockTime = stream.readInt(4); 165 | 166 | return transaction; 167 | } 168 | 169 | public version: number; 170 | public inputs: TransactionInput[]; 171 | public outputs: TransactionOutput[]; 172 | public lockTime: number; 173 | constructor(version?: number, inputs?: TransactionInput[], outputs?: TransactionOutput[], lockTime?: number) { 174 | this.version = version || 1; 175 | this.inputs = inputs || []; 176 | this.outputs = outputs || []; 177 | this.lockTime = lockTime || 0; 178 | } 179 | 180 | public toHex() { 181 | const sink = new ArraySink(); 182 | this.serializeInto(sink); 183 | return Buffer.from(sink.rawBytes).toString("hex") 184 | } 185 | 186 | public serializeInto(stream: ArraySink) { 187 | stream.writeInt(this.version, 4); 188 | 189 | stream.writeVarInt(this.inputs.length); 190 | for (let i = 0, input; input = this.inputs[i]; i++) { 191 | stream.writeHexBytes(input.previousTxHash); 192 | stream.writeInt(input.previousTxOutIndex, 4); 193 | stream.writeString(input.scriptSig); 194 | stream.writeHexBytes(input.sequenceNo); 195 | if (input.satoshis && input.incomplete) { 196 | stream.writeInt(input.satoshis, 8); 197 | } 198 | } 199 | 200 | stream.writeVarInt(this.outputs.length); 201 | for (let i = 0, output; output = this.outputs[i]; i++) { 202 | stream.writeInt(output.value, 8); 203 | stream.writeString(output.scriptPubKey); 204 | } 205 | 206 | stream.writeInt(this.lockTime, 4); 207 | } 208 | } 209 | 210 | // tslint:disable-next-line: max-classes-per-file 211 | export class ArraySink { 212 | public rawBytes: number[]; 213 | constructor(rawBytes?: number[]) { 214 | this.rawBytes = rawBytes || []; 215 | } 216 | 217 | public writeByte(byte: number) { 218 | this.rawBytes.push(byte); 219 | } 220 | public writeBytes(bytes: number[]) { 221 | Array.prototype.push.apply(this.rawBytes, bytes); 222 | } 223 | public writeInt(number: number, count: number) { 224 | this.writeBytes(LittleEndian.encode(number, count)); 225 | } 226 | public writeVarInt(num: number) { 227 | if (num < 0xfd) { 228 | this.writeByte(num); 229 | } else if (num <= 0xffff) { 230 | this.writeByte(0xfd); 231 | this.writeBytes(LittleEndian.encode(num, 2)); 232 | } else { 233 | throw new Error("Not implemented."); 234 | } 235 | } 236 | public writeString(bytes: number[]) { 237 | this.writeVarInt(bytes.length); 238 | this.writeBytes(bytes); 239 | } 240 | public writeHexBytes(text: string) { 241 | this.writeBytes(Hex.decode(text).reverse()) 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /lib/script.ts: -------------------------------------------------------------------------------- 1 | export class Script { 2 | public static opcodes = { 3 | OP_0: 0, 4 | OP_16: 96, 5 | OP_PUSHDATA1: 76, 6 | OP_PUSHDATA2: 77, 7 | OP_PUSHDATA4: 78, 8 | OP_1NEGATE: 79, 9 | OP_RETURN: 106, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /lib/slptokentype1.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from "bignumber.js"; 2 | import { NFT1, TokenType1 } from "slp-mdm"; 3 | 4 | export class SlpTokenType1 { 5 | static get lokadIdHex() { return "534c5000"; } 6 | 7 | public static buildGenesisOpReturn( 8 | ticker: string|null, 9 | name: string|null, 10 | documentUrl: string|null, 11 | documentHashHex: string|null, 12 | decimals: number, 13 | batonVout: number|null, 14 | initialQuantity: BigNumber, 15 | type= 0x01, 16 | ) { 17 | if (decimals === null || decimals === undefined) { 18 | throw Error("Decimals property must be in range 0 to 9"); 19 | } 20 | if (ticker !== null && typeof ticker !== "string") { 21 | throw Error("ticker must be a string"); 22 | } 23 | if (name !== null && typeof name !== "string") { 24 | throw Error("name must be a string"); 25 | } 26 | let res: Buffer; 27 | switch (type) { 28 | case 0x01: 29 | res = TokenType1.genesis( 30 | ticker || "", 31 | name || "", 32 | documentUrl || "", 33 | documentHashHex || "", 34 | decimals || 0, 35 | batonVout, 36 | initialQuantity 37 | ); 38 | break; 39 | case 0x41: 40 | if (! initialQuantity.isEqualTo(1)) { 41 | throw Error("nft1 child output quantity must be equal to 1"); 42 | } 43 | res = NFT1.Child.genesis(ticker || "", 44 | name || "", 45 | documentUrl || "", 46 | documentHashHex || "" 47 | ); 48 | break; 49 | case 0x81: 50 | res = NFT1.Group.genesis( 51 | ticker || "", 52 | name || "", 53 | documentUrl || "", 54 | documentHashHex || "", 55 | decimals || 0, 56 | batonVout, 57 | initialQuantity 58 | ); 59 | break; 60 | default: 61 | throw Error("unsupported token type"); 62 | } 63 | if (res.length > 223) { 64 | throw Error("Script too long, must be less than or equal to 223 bytes."); 65 | } 66 | return res; 67 | } 68 | 69 | public static buildSendOpReturn(tokenIdHex: string, outputQtyArray: BigNumber[], type= 0x01) { 70 | switch (type) { 71 | case 0x01: 72 | return TokenType1.send(tokenIdHex, outputQtyArray); 73 | case 0x41: 74 | if (outputQtyArray.length !== 1) { 75 | throw Error("nft1 child must have exactly 1 output quantity"); 76 | } 77 | if (! outputQtyArray[0].isEqualTo(1)) { 78 | throw Error("nft1 child output quantity must be equal to 1"); 79 | } 80 | return NFT1.Child.send(tokenIdHex, outputQtyArray); 81 | case 0x81: 82 | return NFT1.Group.send(tokenIdHex, outputQtyArray); 83 | default: 84 | throw Error("unsupported token type"); 85 | } 86 | } 87 | 88 | public static buildMintOpReturn(tokenIdHex: string, batonVout: number|null, mintQuantity: BigNumber, type= 0x01) { 89 | switch (type) { 90 | case 0x01: 91 | return TokenType1.mint(tokenIdHex, batonVout, mintQuantity); 92 | case 0x41: 93 | throw Error("nft1 child cannot mint"); 94 | case 0x81: 95 | return NFT1.Group.mint(tokenIdHex, batonVout, mintQuantity); 96 | default: 97 | throw Error("unsupported token type"); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/trustedvalidator.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig } from "axios"; 2 | import { GetRawTransactionsAsync, logger } from "../index"; 3 | import { Crypto } from "./crypto"; 4 | import { SlpValidator } from "./slp"; 5 | 6 | interface Validation { validity: boolean|null; invalidReason: string|null; tokenIdHex?: string; tokenTypeHex?: number; } 7 | 8 | export class TrustedValidator implements SlpValidator { 9 | public getRawTransactions: GetRawTransactionsAsync; 10 | public cachedValidations: { [txid: string]: Validation }; 11 | public logger: logger = { log: (s: string) => null }; 12 | public slpdbUrl: string; 13 | 14 | constructor({slpdbUrl= "https://slpdb.fountainhead.cash", logger, getRawTransactions}: 15 | { slpdbUrl?: string; logger?: logger; getRawTransactions: GetRawTransactionsAsync}) { 16 | if (logger) { 17 | this.logger = logger; 18 | } 19 | this.getRawTransactions = getRawTransactions; 20 | this.slpdbUrl = slpdbUrl; 21 | this.cachedValidations = {}; 22 | } 23 | 24 | public addValidationFromStore(hex: string, isValid: boolean, tokenIdHex?: string, tokenTypeHex?: number) { 25 | const id = Crypto.txid(Buffer.from(hex, "hex")).toString("hex"); 26 | if (!this.cachedValidations[id]) { 27 | this.cachedValidations[id] = { validity: isValid, invalidReason: null, tokenIdHex, tokenTypeHex }; 28 | } 29 | } 30 | 31 | public async isValidSlpTxid(txid: string, tokenIdFilter?: string, tokenTypeFilter?: number): Promise { 32 | this.logger.log("SLPJS Validating (via SLPDB): " + txid); 33 | const valid = await this._isValidSlpTxid(txid, tokenIdFilter, tokenTypeFilter); 34 | this.logger.log("SLPJS Result (via SLPDB): " + valid + " (" + txid + ")"); 35 | if (!valid && this.cachedValidations[txid].invalidReason) { 36 | this.logger.log("SLPJS Invalid Reason (via SLPDB): " + this.cachedValidations[txid].invalidReason); 37 | } else if (!valid) { 38 | this.logger.log("SLPJS Invalid Reason (via SLPDB): unknown (result is user supplied)"); 39 | } 40 | return valid; 41 | } 42 | 43 | public async validateSlpTransactions(txids: string[]): Promise { 44 | const res = []; 45 | for (const txid of txids) { 46 | res.push((await this.isValidSlpTxid(txid)) ? txid : ""); 47 | } 48 | return res.filter((id: string) => id.length > 0); 49 | } 50 | 51 | private async _isValidSlpTxid(txid: string, tokenIdFilter?: string, tokenTypeFilter?: number): Promise { 52 | if (this.cachedValidations[txid]) { 53 | if (tokenIdFilter && tokenIdFilter !== this.cachedValidations[txid].tokenIdHex) { 54 | this.cachedValidations[txid].invalidReason = "Incorrect tokenIdFilter"; 55 | return false; 56 | } else if (tokenTypeFilter && tokenTypeFilter !== this.cachedValidations[txid].tokenTypeHex) { 57 | this.cachedValidations[txid].invalidReason = "Incorrect tokenTypeFilter"; 58 | return false; 59 | } else if (typeof this.cachedValidations[txid].validity === "boolean") { 60 | return this.cachedValidations[txid].validity!; 61 | } else { 62 | throw Error("Validation cache is corrupt."); 63 | } 64 | } 65 | 66 | // todo abstract this 67 | const q = { 68 | v: 3, 69 | q: { 70 | db: ["c", "u"], 71 | aggregate: [ 72 | { $match: { "tx.h": txid }}, 73 | { $project: { validity: "$slp.valid", invalidReason: "$slp.invalidReason", tokenTypeHex: "$slp.detail.versionType", tokenIdHex: "$slp.detail.tokenIdHex" }}, 74 | ], 75 | limit: 1, 76 | }, 77 | }; 78 | const data = Buffer.from(JSON.stringify(q)).toString("base64"); 79 | const config: AxiosRequestConfig = { 80 | method: "GET", 81 | url: this.slpdbUrl + "/q/" + data, 82 | }; 83 | const response: { c: Validation[], u: Validation[] } = (await axios(config)).data; 84 | let result!: Validation; 85 | if (response.c.length > 0) { 86 | result = response.c[0]; 87 | } else if (response.u.length > 0) { 88 | result = response.u[0]; 89 | } else { 90 | result = { validity: false, invalidReason: "Transaction not found in SLPDB." }; 91 | } 92 | if (result) { 93 | this.cachedValidations[txid] = result; 94 | } 95 | return result.validity!; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import * as Bchaddr from "bchaddrjs-slp"; 2 | import BigNumber from "bignumber.js"; 3 | import { AddressUtxoResult } from "bitcoin-com-rest"; 4 | import { SlpAddressUtxoResult, SlpPaymentRequest, utxo } from "../index"; 5 | 6 | export class Utils { 7 | public static isCashAddress(address: string): any { 8 | try { 9 | return Bchaddr.isCashAddress(address); 10 | } catch (_) { 11 | return false; 12 | } 13 | } 14 | 15 | public static toCashAddress(address: string) { 16 | return Bchaddr.toCashAddress(address); 17 | } 18 | 19 | public static slpAddressFromHash160(hash: Uint8Array, network= "mainnet", addressType= "p2pkh"): string { 20 | if (network !== "mainnet" && network !== "testnet") { 21 | throw Error("Invalid network given."); 22 | } 23 | if (addressType !== "p2pkh" && addressType !== "p2sh") { 24 | throw Error("Invalid address type given."); 25 | } 26 | return Bchaddr.encodeAsSlpaddr({ hash, type: addressType, network, format: "" }); 27 | } 28 | 29 | public static isSlpAddress(address: string) { 30 | try { 31 | return Bchaddr.isSlpAddress(address); 32 | } catch (_) { 33 | return false; 34 | } 35 | } 36 | 37 | public static toSlpAddress(address: string) { 38 | return Bchaddr.toSlpAddress(address); 39 | } 40 | 41 | public static toRegtestAddress(address: string) { 42 | return Bchaddr.toRegtestAddress(address); 43 | } 44 | 45 | public static isLegacyAddress(address: string) { 46 | try { 47 | return Bchaddr.isLegacyAddress(address); 48 | } catch (_) { 49 | return false; 50 | } 51 | } 52 | 53 | public static toLegacyAddress(address: string) { 54 | return Bchaddr.toLegacyAddress(address); 55 | } 56 | 57 | public static isMainnet(address: string) { 58 | if (Bchaddr.decodeAddress(address).network === "mainnet") { 59 | return true; 60 | } 61 | return false; 62 | } 63 | 64 | public static txnBuilderString(address: string) { 65 | return Utils.isMainnet(address) ? "bitcoincash" : "bchtest"; 66 | } 67 | 68 | public static mapToSlpAddressUtxoResultArray(result: AddressUtxoResult) { 69 | return result.utxos.map(txo => { 70 | return { 71 | satoshis: txo.satoshis, 72 | txid: txo.txid, 73 | amount: txo.amount, 74 | confirmations: txo.confirmations, 75 | height: txo.height, 76 | vout: txo.vout, 77 | cashAddress: result.cashAddress, 78 | legacyAddress: result.legacyAddress, 79 | slpAddress: Bchaddr.toSlpAddress(result.legacyAddress), 80 | scriptPubKey: result.scriptPubKey, 81 | } as any as SlpAddressUtxoResult; 82 | }); 83 | } 84 | 85 | public static mapToUtxoArray(utxos: SlpAddressUtxoResult[]) { 86 | return utxos.map(txo => { 87 | return { 88 | satoshis: new BigNumber(txo.satoshis), 89 | wif: txo.wif, 90 | txid: txo.txid, 91 | vout: txo.vout, 92 | slpTransactionDetails: txo.slpTransactionDetails, 93 | slpUtxoJudgement: txo.slpUtxoJudgement, 94 | slpUtxoJudgementAmount: txo.slpUtxoJudgementAmount, 95 | } as utxo; 96 | }); 97 | } 98 | 99 | public static getPushDataOpcode(data: number[]|Buffer) { 100 | let length = data.length; 101 | 102 | if (length === 0) { 103 | return [0x4c, 0x00]; 104 | } else if (length < 76) { 105 | return length; 106 | } else if (length < 256) { 107 | return [0x4c, length]; 108 | } 109 | throw Error("Pushdata too large"); 110 | } 111 | 112 | public static int2FixedBuffer(amount: BigNumber) { 113 | try { 114 | amount.absoluteValue(); 115 | } catch (_) { 116 | throw Error("Amount must be an instance of BigNumber"); 117 | } 118 | 119 | let hex: string = amount.toString(16); 120 | hex = hex.padStart(16, "0"); 121 | return Buffer.from(hex, "hex"); 122 | } 123 | 124 | public static buffer2BigNumber(amount: Buffer) { 125 | if (amount.length < 5 || amount.length > 8) { 126 | throw Error("Buffer must be between 4-8 bytes in length"); 127 | } 128 | return (new BigNumber(amount.readUInt32BE(0).toString())).multipliedBy(2 ** 32).plus(amount.readUInt32BE(4).toString()); 129 | } 130 | 131 | public static buildSlpUri(address: string, amountBch?: number, amountToken?: number, tokenId?: string): string { 132 | let uri = ""; 133 | if (!this.isSlpAddress(address)) { 134 | throw Error("Not a valid SLP address"); 135 | } 136 | if (address.startsWith("simpleledger:")) { 137 | uri = uri.concat(address); 138 | } else { 139 | uri = uri.concat("simpleledger:" + address); 140 | } 141 | if (amountBch || amountToken) { 142 | uri = uri.concat("?"); 143 | } 144 | let n: number = 0; 145 | if (amountBch) { 146 | uri = uri.concat("amount=" + amountBch.toString()); 147 | n++; 148 | } 149 | if (amountToken) { 150 | if (!tokenId) { 151 | throw Error("Missing tokenId parameter"); 152 | } 153 | let re = /^([A-Fa-f0-9]{2}){32,32}$/; 154 | if (!re.test(tokenId)) { 155 | throw Error("TokenId is invalid, must be 32-byte hexidecimal string"); 156 | } 157 | if (n > 0) { 158 | uri = uri.concat("&amount" + n.toString() + "=" + amountToken.toString() + "-" + tokenId); 159 | } else { 160 | uri = uri.concat("amount" + "=" + amountToken.toString() + "-" + tokenId); 161 | } 162 | } 163 | return uri; 164 | } 165 | 166 | public static parseSlpUri(uri: string): SlpPaymentRequest { 167 | if (!uri.startsWith("simpleledger:")) { 168 | throw Error("Input does not start with 'simpleledger:'"); 169 | } else { 170 | uri = uri.replace("simpleledger:", ""); 171 | } 172 | let splitUri = uri.split("?"); 173 | if (splitUri.length > 2) { 174 | throw Error("Cannot have character '?' more than once."); 175 | } 176 | if (!this.isSlpAddress(splitUri[0])) { 177 | throw Error("Address is not an SLP formatted address."); 178 | } 179 | let result: SlpPaymentRequest = { address: "simpleledger:" + splitUri[0] }; 180 | if (splitUri.length > 1) { 181 | splitUri = splitUri[1].split("&"); 182 | let paramNames: string[] = []; 183 | splitUri.forEach(param => { 184 | if (param.split("=").length === 2) { 185 | let str = param.split("="); 186 | if (paramNames.includes(str[0])) { 187 | throw Error("Cannot have duplicate parameter names in URI string"); 188 | } 189 | if (str[0].startsWith("amount") && str[1].split("-").length === 1) { 190 | result.amountBch = parseFloat(str[1]); 191 | } else if (str[0].startsWith("amount") && str[1].split("-").length > 1) { 192 | let p = str[1].split("-"); 193 | if (p.length > 2) { 194 | throw Error("Token flags params not yet implemented."); 195 | } 196 | let re = /^([A-Fa-f0-9]{2}){32,32}$/; 197 | if (p.length > 1 && !re.test(p[1])) { 198 | throw Error("Token id in URI is not a valid 32-byte hexidecimal string"); 199 | } 200 | result.amountToken = parseFloat(p[0]); 201 | result.tokenId = p[1]; 202 | } 203 | paramNames.push(str[0]); 204 | } 205 | }); 206 | } 207 | return result; 208 | } 209 | 210 | public static get_BIP62_locktime_hex(unixtime: number) { 211 | return Utils.convertBE2LE32(unixtime.toString(16)); 212 | } 213 | 214 | // convert Big Endian to Little Endian for the given Hex string 215 | public static convertBE2LE32(hex: string) { 216 | if (hex === "") { return null; } 217 | if (!Utils.isHexString(hex)) { return null; } 218 | if (hex.length % 2 > 0) { 219 | hex = "0" + hex; 220 | } 221 | hex = hex.match(/.{2}/g)!.reverse().join(""); 222 | return hex; 223 | } 224 | 225 | // check validation of hex string 226 | public static isHexString(hex: string) { 227 | let regexp = /^[0-9a-fA-F]+$/; 228 | if (!regexp.test(hex)) { return false; } 229 | return true; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /lib/vendors.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare module "bchaddrjs-slp" { 3 | export function isCashAddress(address: string): boolean; 4 | export function toCashAddress(address: string): string; 5 | export function isLegacyAddress(address: string): boolean; 6 | export function toLegacyAddress(address: string): string; 7 | export function isSlpAddress(address: string): boolean; 8 | export function toSlpAddress(address: string): string; 9 | export function toRegtestAddress(address: string): string; 10 | export function decodeAddress(address: string): AddressDetails; 11 | export function encodeAsSlpaddr(decoded: AddressDetails): string; 12 | 13 | export interface AddressDetails { 14 | hash: Uint8Array; 15 | format: string; 16 | network: string; 17 | type: string; 18 | } 19 | } 20 | 21 | 22 | declare module "bitcore-lib-cash" { 23 | // Type definitions for bitcore-lib 0.15 24 | // Project: https://github.com/bitpay/bitcore-lib 25 | // Definitions by: Lautaro Dragan 26 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 27 | 28 | // TypeScript Version: 2.2 29 | 30 | /// 31 | 32 | export namespace crypto { 33 | class BN { 34 | static fromNumber(input_satoshis: number): any; 35 | } 36 | namespace ECDSA { 37 | function sign(message: Buffer, key: PrivateKey): Signature; 38 | function verify(hashbuf: Buffer, sig: Signature, pubkey: PublicKey, endian?: 'little'): boolean; 39 | } 40 | namespace Hash { 41 | function sha256(buffer: Buffer): Uint8Array; 42 | } 43 | namespace Random { 44 | function getRandomBuffer(size: number): Buffer; 45 | } 46 | namespace Point {} 47 | class Signature { 48 | static fromDER(sig: Buffer): Signature; 49 | static fromString(data: string): Signature; 50 | SIGHASH_ALL: number; 51 | toString(): string; 52 | } 53 | } 54 | 55 | export class Transaction { 56 | inputs: Input[]; 57 | outputs: Output[]; 58 | toObject(): any; 59 | toBuffer(): Buffer; 60 | readonly id: string; 61 | readonly hash: string; 62 | nid: string; 63 | static Output: any; 64 | static Input: any; 65 | static Sighash: any; 66 | 67 | constructor(serialized?: any); 68 | 69 | from(utxos: Transaction.UnspentOutput[]): Transaction; 70 | to(address: Address | string, amount: number): Transaction; 71 | change(address: Address | string): Transaction; 72 | sign(privateKey: PrivateKey | string): Transaction; 73 | applySignature(sig: crypto.Signature): Transaction; 74 | addData(data: Buffer): this; 75 | serialize(): string; 76 | } 77 | 78 | export namespace Transaction { 79 | class UnspentOutput { 80 | static fromObject(o: object): UnspentOutput; 81 | 82 | readonly address: Address; 83 | readonly txId: string; 84 | readonly outputIndex: number; 85 | readonly script: Script; 86 | readonly satoshis: number; 87 | 88 | constructor(data: object); 89 | 90 | inspect(): string; 91 | toObject(): this; 92 | toString(): string; 93 | toBuffer(): Buffer; 94 | } 95 | } 96 | 97 | export class Block { 98 | hash: string; 99 | height: number; 100 | transactions: Transaction[]; 101 | header: { 102 | time: number; 103 | prevHash: string; 104 | }; 105 | 106 | constructor(data: Buffer | object); 107 | } 108 | 109 | export class PrivateKey { 110 | readonly publicKey: PublicKey; 111 | 112 | constructor(key: string); 113 | } 114 | 115 | export class PublicKey { 116 | constructor(source: string); 117 | 118 | static fromPrivateKey(privateKey: PrivateKey): PublicKey; 119 | 120 | toBuffer(): Buffer; 121 | toDER(): Buffer; 122 | } 123 | 124 | export interface Output { 125 | satoshis: number; 126 | _scriptBuffer: Buffer; 127 | readonly script: any; 128 | } 129 | 130 | export namespace Script { 131 | const types: { 132 | DATA_OUT: string; 133 | }; 134 | function buildPublicKeyHashOut(address: Address): Script; 135 | } 136 | 137 | export class Script { 138 | static fromAddress(address: Address|string): Script; 139 | constructor(script: Buffer); 140 | fromBuffer(buffer: Buffer): Script; 141 | toBuffer(): Buffer; 142 | toAddress(network: any): Address; 143 | fromString(hex: string): Script; 144 | fromASM(asm: string): string; 145 | toASM(): string; 146 | fromHex(hex: string): string 147 | toHex(): string; 148 | chunks: Chunk[]; 149 | } 150 | 151 | export interface Chunk { 152 | buf: Buffer; 153 | len: number; 154 | opcodenum: number; 155 | } 156 | 157 | export interface Util { 158 | readonly buffer: { 159 | reverse(a: any): any; 160 | }; 161 | } 162 | 163 | export namespace Networks { 164 | interface Network { 165 | readonly name: string; 166 | readonly alias: string; 167 | } 168 | 169 | const livenet: Network; 170 | const mainnet: Network; 171 | const testnet: Network; 172 | 173 | function add(data: any): Network; 174 | function remove(network: Network): void; 175 | function get(args: string | number | Network, keys: string | string[]): Network; 176 | } 177 | 178 | export class Address { 179 | toString(fmt?: string): string; 180 | } 181 | 182 | export class Input { 183 | output: any; 184 | prevTxId: any; 185 | script: Script; 186 | outputIndex: number; 187 | getSignatures(txn: Transaction, privateKey: PrivateKey, input_index: number, sigHashType: number): any; 188 | setScript(script: Script): void; 189 | _scriptBuffer: Buffer; 190 | } 191 | } 192 | 193 | // export interface TxnInput { 194 | // redeemScript: any; 195 | // output: any; 196 | // script: Script; 197 | // _scriptBuffer: Buffer; 198 | // prevTxId: Buffer; 199 | // outputIndex: number; 200 | // sequenceNumber: number; 201 | // setScript(script: Script): void; 202 | // getSignatures(transaction: Transaction, privKey: PrivateKey, index: number, sigtype?: number, hashData?: Buffer): any; 203 | // } 204 | 205 | // export interface Script { 206 | // fromBuffer(buffer: Buffer): Script; 207 | // toBuffer(): Buffer; 208 | // toAddress(network: any): Address; 209 | // fromAddress(address: Address): Script; 210 | // fromString(hex: string): Script; 211 | // fromASM(asm: string): string; 212 | // toASM(): string; 213 | // fromHex(hex: string): string 214 | // toHex(): string; 215 | // chunks: Chunk[]; 216 | // } 217 | 218 | // export interface Chunk { 219 | // buf: Buffer; 220 | // len: number; 221 | // opcodenum: number; 222 | // } 223 | 224 | // export interface TxnOutput { 225 | // _scriptBuffer: Buffer; 226 | // script: Script; 227 | // satoshis: number; 228 | // } 229 | 230 | // export class crypto { 231 | // static BN: any; 232 | // } 233 | 234 | // export class Transaction { 235 | // inputs: Input[]; 236 | // outputs: Output[]; 237 | // readonly id: string; 238 | // readonly hash: string; 239 | // nid: string; 240 | 241 | // constructor(serialized?: any); 242 | 243 | // from(utxos: Transaction.UnspentOutput[]): Transaction; 244 | // to(address: Address | string, amount: number): Transaction; 245 | // change(address: Address | string): Transaction; 246 | // sign(privateKey: PrivateKey | string): Transaction; 247 | // applySignature(sig: crypto.Signature): Transaction; 248 | // addData(data: Buffer): this; 249 | // serialize(): string; 250 | // } 251 | 252 | // export class PrivateKey { 253 | // constructor(key: string); 254 | // } 255 | 256 | // export class Script { 257 | // static fromAddress(arg0: any): any; 258 | // constructor(script: Buffer); 259 | // } 260 | 261 | // export interface Transaction { 262 | // inputs: TxnInput[]; 263 | // outputs: TxnOutput[]; 264 | // toObject(): any; 265 | // serialize(unsafe?: boolean): string; 266 | // sign(key: PrivateKey): Promise; 267 | // hash: string; 268 | // id: string; 269 | // } 270 | 271 | // export interface Networks { 272 | // livenet: any; 273 | // } 274 | 275 | // export interface Address { 276 | // toString(format: string): string; 277 | // } 278 | // } 279 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slpjs", 3 | "version": "0.27.11", 4 | "description": "Simple Ledger Protocol (SLP) JavaScript Library", 5 | "main": "index.js", 6 | "files": [ 7 | "index.d.ts", 8 | "lib/*.js", 9 | "lib/*.d.ts", 10 | "dist/" 11 | ], 12 | "scripts": { 13 | "test": "mocha", 14 | "coverage": "nyc npm test && nyc report --reporter=text-lcov | coveralls", 15 | "build": "npx tsc && mkdirp dist && browserify index.js --standalone slpjs > dist/slpjs.js && uglifyjs dist/slpjs.js > dist/slpjs.min.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/simpleledger/slpjs.git" 20 | }, 21 | "keywords": [ 22 | "bitcoin", 23 | "bch", 24 | "bitcoin cash", 25 | "tokens", 26 | "slp", 27 | "ledger", 28 | "simpleledger" 29 | ], 30 | "author": "James Cramer", 31 | "license": "ISC", 32 | "unpkg": "dist/slpjs.min.js", 33 | "bugs": { 34 | "url": "https://github.com/simpleledger/slpjs/issues" 35 | }, 36 | "homepage": "https://github.com/simpleledger/slpjs#readme", 37 | "devDependencies": { 38 | "@istanbuljs/nyc-config-typescript": "^0.1.3", 39 | "@types/mocha": "^7.0.2", 40 | "@types/node": "^10.17.26", 41 | "bitbox-sdk": "^8.11.2", 42 | "bitcoin-rpc-promise": "^2.1.6", 43 | "bitcore-lib-cash": "^9.0.0", 44 | "browserify": "^16.5.1", 45 | "grpc-bchrpc-node": "^0.11.5", 46 | "mkdirp": "^0.5.5", 47 | "mocha": "^7.2.0", 48 | "nyc": "^15.1.0", 49 | "slp-unit-test-data": "git+https://github.com/simpleledger/slp-unit-test-data.git#8c942eacfae12686dcf1f3366321445a4fba73e7", 50 | "source-map-support": "^0.5.19", 51 | "ts-node": "^7.0.1", 52 | "typescript": "^3.9.5", 53 | "typescript-tslint-plugin": "^0.5.5", 54 | "uglify-es": "^3.3.9" 55 | }, 56 | "peerDependencies": { 57 | "bitbox-sdk": "^8.11.2", 58 | "bitcore-lib-cash": "^9.0.0" 59 | }, 60 | "dependencies": { 61 | "@types/crypto-js": "^3.1.47", 62 | "@types/lodash": "^4.14.156", 63 | "@types/randombytes": "^2.0.0", 64 | "@types/socket.io": "^2.1.8", 65 | "@types/socket.io-client": "^1.4.33", 66 | "@types/wif": "^2.0.1", 67 | "axios": "^0.21.1", 68 | "bchaddrjs-slp": "0.2.8", 69 | "bignumber.js": "9.0.0", 70 | "crypto-js": "^4.0.0", 71 | "grpc-bchrpc": "^0.0.10", 72 | "lodash": "^4.17.15", 73 | "slp-mdm": "0.0.6" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/bchdnetwork.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { BigNumber } from "bignumber.js"; 3 | import { BITBOX } from "bitbox-sdk"; 4 | import { GrpcClient } from "grpc-bchrpc-node"; 5 | import { BchdNetwork } from "../lib/bchdnetwork"; 6 | import { GetRawTransactionsAsync, LocalValidator } from "../lib/localvalidator"; 7 | 8 | describe("BchdNetwork (mainnet)", () => { 9 | const bitbox = new BITBOX(); 10 | describe("getTokenInformation()", () => { 11 | const client = new GrpcClient(); 12 | const getRawTransactions: GetRawTransactionsAsync = async (txids: string[]) => { 13 | const getRawTransaction = async (txid: string) => { 14 | console.log(`Downloading: ${txid}`); 15 | return await client.getRawTransaction({hash: txid, reversedHashOrder: true}); 16 | }; 17 | return (await Promise.all( 18 | txids.map((txid) => getRawTransaction(txid)))) 19 | .map((res) => Buffer.from(res.getTransaction_asU8()).toString("hex")); 20 | }; 21 | 22 | const logger = console; 23 | const validator = new LocalValidator(bitbox, getRawTransactions, logger); 24 | const network = new BchdNetwork({BITBOX: bitbox, client, validator, logger}); 25 | it("returns token information for a given valid tokenId", async () => { 26 | const tokenId = "667b28d5885717e6d164c832504ae6b0c4db3c92072119ddfc5ff0db2c433456"; 27 | const tokenInfo = await network.getTokenInformation(tokenId, true); 28 | const expectedTokenInfo = { 29 | tokenIdHex: "667b28d5885717e6d164c832504ae6b0c4db3c92072119ddfc5ff0db2c433456", 30 | transactionType: "GENESIS", 31 | versionType: 1, 32 | symbol: "BCH", 33 | name: "Bitcoin Cash", 34 | documentUri: "", 35 | documentSha256: null, 36 | decimals: 8, 37 | containsBaton: true, 38 | batonVout: 2, 39 | genesisOrMintQuantity: new BigNumber("21000000"), 40 | }; 41 | assert.deepEqual(tokenInfo, expectedTokenInfo); 42 | }); 43 | it("throws when tokenId is not found", async () => { 44 | const tokenId = "000028d5885717e6d164c832504ae6b0c4db3c92072119ddfc5ff0db2c433456"; 45 | let threw = false; 46 | try { 47 | await network.getTokenInformation(tokenId); 48 | } catch (error) { 49 | threw = true; 50 | assert.equal(error.message, "5 NOT_FOUND: transaction not found"); 51 | } finally { assert.equal(threw, true); } 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/bitboxnetwork.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { BigNumber } from "bignumber.js"; 3 | import { BITBOX } from "bitbox-sdk"; 4 | import { BitboxNetwork } from "../lib/bitboxnetwork"; 5 | 6 | describe("BitboxNetwork (mainnet)", () => { 7 | const bitbox = new BITBOX(); 8 | describe("getTokenInformation()", () => { 9 | const net = new BitboxNetwork(bitbox); 10 | it("returns token information for a given valid tokenId", async () => { 11 | const tokenId = "667b28d5885717e6d164c832504ae6b0c4db3c92072119ddfc5ff0db2c433456"; 12 | const tokenInfo = await net.getTokenInformation(tokenId, true); 13 | const expectedTokenInfo = { 14 | tokenIdHex: "667b28d5885717e6d164c832504ae6b0c4db3c92072119ddfc5ff0db2c433456", 15 | transactionType: "GENESIS", 16 | versionType: 1, 17 | symbol: "BCH", 18 | name: "Bitcoin Cash", 19 | documentUri: "", 20 | documentSha256: null, 21 | decimals: 8, 22 | containsBaton: true, 23 | batonVout: 2, 24 | genesisOrMintQuantity: new BigNumber("21000000"), 25 | }; 26 | assert.deepEqual(tokenInfo, expectedTokenInfo); 27 | }); 28 | it("throws when tokenId is not found", async () => { 29 | const tokenId = "000028d5885717e6d164c832504ae6b0c4db3c92072119ddfc5ff0db2c433456"; 30 | let threw = false; 31 | try { 32 | await net.getTokenInformation(tokenId); 33 | } catch (error) { 34 | threw = true; 35 | assert.equal(error.message, "No such mempool or blockchain transaction. Use gettransaction for wallet transactions."); 36 | } finally { assert.equal(threw, true); } 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/bitdbnetwork.test.ts: -------------------------------------------------------------------------------- 1 | import { BitdbNetwork } from "../lib/bitdbnetwork"; 2 | 3 | import * as assert from "assert"; 4 | import { BigNumber } from "bignumber.js"; 5 | 6 | describe("BitdbNetwork", function() { 7 | describe("getTokenInformation()", function() { 8 | //console.log(JSON.stringify(BitdbNetwork)); 9 | const net = new BitdbNetwork(); 10 | it("returns token information for a given valid tokenId", async () => { 11 | const tokenId = "667b28d5885717e6d164c832504ae6b0c4db3c92072119ddfc5ff0db2c433456"; 12 | const tokenInfo = await net.getTokenInformation(tokenId); 13 | const expectedTokenInfo = { 14 | timestamp: "2019-01-19 14:33", 15 | tokenIdHex: "667b28d5885717e6d164c832504ae6b0c4db3c92072119ddfc5ff0db2c433456", 16 | transactionType: "GENESIS", 17 | versionType: 1, 18 | symbol: "BCH", 19 | name: "Bitcoin Cash", 20 | documentUri: "", 21 | documentSha256: null, 22 | decimals: 8, 23 | containsBaton: true, 24 | batonVout: 2, 25 | genesisOrMintQuantity: new BigNumber("21000000"), 26 | }; 27 | assert.deepEqual(tokenInfo, expectedTokenInfo); 28 | }); 29 | it("throws when tokenId is not found", async () => { 30 | const tokenId = "000028d5885717e6d164c832504ae6b0c4db3c92072119ddfc5ff0db2c433456"; 31 | let threw = false; 32 | try { 33 | await net.getTokenInformation(tokenId); 34 | } catch (error) { 35 | threw = true; 36 | assert.equal(error.message, "Token not found"); 37 | } finally { assert.equal(threw, true); } 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/crypto.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { Crypto } from "../lib/crypto"; 3 | 4 | describe("Crypto", () => { 5 | describe("txid()", () => { 6 | it("Transaction hash computes properly", () => { 7 | const txn = Buffer.from("010000000195e0023eb5592ca1ddf53c5faa3b996d77e858c9c6fc6463805c447e4c4e815b010000006a473044022005f782e7e3f5b3ec1b244f1dc2d579bee762978d17d32a1b8c9e4bc7d99254eb02202cf1ea85affb4873b6e673116f56d8c94069cdb36ec9f30f93a9cf68be962b18412103363471f8c0366aa48519defbd59b1744b22e4a096d15001c2860c84e8454bc75feffffff025a433000000000001976a914e2f118495cb33222ccbafc5cf7ddc69357fc982088acc07dbb0b000000001976a9140e8f40ab1a164ba2a45e28d2b73f8d9047a8d2b988acc2910900", "hex"); 8 | const expectedTxid = "34e3d749803e0b14e41a2c8df559eed10e82898c5382563a3bbb9cc1fe3ee12d"; 9 | const txid = Crypto.txid(txn).toString("hex"); 10 | assert.equal(txid, expectedTxid); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/global.d.ts: -------------------------------------------------------------------------------- 1 | export interface SlpValidityUnitTest { 2 | description: string; 3 | when: SlpTestTxn[]; 4 | should: SlpTestTxn[]; 5 | allow_inconclusive: boolean; 6 | inconclusive_reason: string; 7 | } 8 | 9 | export interface SlpTestTxn { 10 | tx: string; 11 | valid: boolean; 12 | } -------------------------------------------------------------------------------- /test/localvalidator.test.ts: -------------------------------------------------------------------------------- 1 | import { Crypto } from "../lib/crypto"; 2 | import { GetRawTransactionsAsync, LocalValidator } from "../lib/localvalidator"; 3 | import { SlpTestTxn, SlpValidityUnitTest } from "./global"; 4 | 5 | import * as assert from "assert"; 6 | import { BITBOX } from "bitbox-sdk"; 7 | import "mocha"; 8 | 9 | const bitbox = new BITBOX(); 10 | const txUnitTestData: SlpValidityUnitTest[] = require("slp-unit-test-data/tx_input_tests.json"); 11 | 12 | describe("Slp", () => { 13 | describe("isValidSlpTxid() -- SLP Transaction Validation Unit Tests", () => { 14 | txUnitTestData.forEach((test) => { 15 | it(test.description, async () => { 16 | 17 | // Create method for serving up the unit test transactions 18 | const getRawUnitTestTransactions: GetRawTransactionsAsync = async (txids: string[]) => { 19 | const allTxns: SlpTestTxn[] = test.when.concat(test.should); 20 | const txn = allTxns.find((i) => { 21 | const hash = Crypto.txid(Buffer.from(i.tx, "hex")).toString("hex"); 22 | return hash === txids[0]; 23 | }); 24 | if (txn) { 25 | return [txn.tx]; 26 | } 27 | throw Error("Transaction data for the provided txid not found (txid: " + txids[0] + ")"); 28 | }; 29 | 30 | // Create instance of Local Validator 31 | let slpValidator = new LocalValidator(bitbox, getRawUnitTestTransactions); 32 | 33 | // Pre-Load Validator the unit-test inputs 34 | test.when.forEach((w) => { 35 | slpValidator.addValidationFromStore(w.tx, w.valid); 36 | }); 37 | 38 | const txid = Crypto.txid(Buffer.from(test.should[0].tx, "hex")).toString("hex"); 39 | const shouldBeValid = test.should[0].valid; 40 | let isValid; 41 | try { 42 | isValid = await slpValidator.isValidSlpTxid(txid); 43 | } catch (error) { 44 | if (error.message.includes("Transaction data for the provided txid not found") && 45 | test.allow_inconclusive && test.inconclusive_reason === "missing-txn") { 46 | isValid = false; 47 | } else { 48 | throw error; 49 | } 50 | } 51 | 52 | if (isValid === false) { 53 | console.log("invalid reason:", slpValidator.cachedValidations[txid].invalidReason); 54 | } 55 | assert.equal(shouldBeValid, isValid); 56 | }); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/slp.test.ts: -------------------------------------------------------------------------------- 1 | import { Slp } from "../lib/slp"; 2 | import assert from "assert"; 3 | import { BITBOX } from "bitbox-sdk"; 4 | 5 | const bitbox = new BITBOX({ restURL: "https://trest.bitcoin.com/v2/" }); 6 | const scriptUnitTestData = require("slp-unit-test-data/script_tests.json"); 7 | 8 | let slp = new Slp(bitbox); 9 | 10 | describe("Slp", () => { 11 | 12 | describe("parseSlpOutputScript() -- SLP OP_RETURN Unit Tests", () => { 13 | scriptUnitTestData.forEach((test: any)=> { 14 | it(test.msg, () => { 15 | let script = Buffer.from(test.script, "hex"); 16 | let eCode = test.code; 17 | if(eCode) { 18 | assert.throws(() => { slp.parseSlpOutputScript(script) }); 19 | } else { 20 | let parsedOutput = slp.parseSlpOutputScript(script); 21 | assert(typeof parsedOutput, "object"); 22 | } 23 | }); 24 | }); 25 | }); 26 | // describe('buildGenesisTransaction()', () => { 27 | 28 | // }) 29 | // describe('buildMintTransaction()', () => { 30 | 31 | // }) 32 | // describe('buildSendTransaction()', () => { 33 | 34 | // }) 35 | 36 | // let genesisTxid; 37 | // let batonTxid; 38 | // let sendTxid; 39 | 40 | // describe('buildRawGenesisTx()', () => { 41 | // const fundingAddress = "slptest:qpwyc9jnwckntlpuslg7ncmhe2n423304ueqcyw80l"; 42 | // const fundingWif = "cVjzvdHGfQDtBEq7oddDRcpzpYuvNtPbWdi8tKQLcZae65G4zGgy"; 43 | // const tokenReceiverAddress = "slptest:qr0mkh2lf6w4cz79n8rwjtf65e0swqqleu3eyzn6s4"; 44 | // const batonReceiverAddress = "slptest:qpwyc9jnwckntlpuslg7ncmhe2n423304ueqcyw80l"; 45 | // const bchChangeReceiverAddress = "slptest:qpwyc9jnwckntlpuslg7ncmhe2n423304ueqcyw80l"; 46 | 47 | // it('Succeeds in creating a valid genesis transaction with override on validateSlpTransactions', async () => { 48 | // this.timeout(5000); 49 | // let balances = await bitboxNetwork.getAllSlpBalancesAndUtxos(fundingAddress); 50 | // console.log(balances); 51 | // let decimals = 9; 52 | // let initialQty = (new BigNumber(1000000)).times(10**decimals); 53 | 54 | // let genesisOpReturn = slp.buildGenesisOpReturn({ 55 | // ticker: null, 56 | // name: null, 57 | // documentUri: null, 58 | // hash: null, 59 | // decimals: decimals, 60 | // batonVout: 2, 61 | // initialQuantity: initialQty, 62 | // }); 63 | 64 | // balances.nonSlpUtxos.forEach(utxo => utxo.wif = fundingWif) 65 | 66 | // let genesisTxHex = slp.buildRawGenesisTx({ 67 | // slpGenesisOpReturn: genesisOpReturn, 68 | // mintReceiverAddress: tokenReceiverAddress, 69 | // batonReceiverAddress: batonReceiverAddress, 70 | // bchChangeReceiverAddress: bchChangeReceiverAddress, 71 | // input_utxos: balances.nonSlpUtxos 72 | // }); 73 | 74 | // genesisTxid = await BITBOX.RawTransactions.sendRawTransaction(genesisTxHex); 75 | 76 | // let re = /^([A-Fa-f0-9]{2}){32,32}$/; 77 | // console.log(genesisTxHex); 78 | // assert.equal(true, re.test(genesisTxid)); 79 | // assert.equal(true, false); 80 | // }); 81 | // }); 82 | // describe('buildRawSendTx()', () => { 83 | // it('works', () => { 84 | // assert.equal(true, false); 85 | // }); 86 | // }); 87 | // describe('buildRawMintTx()', () => { 88 | // it('works', () => { 89 | // assert.equal(true, false); 90 | // }); 91 | // }); 92 | }); -------------------------------------------------------------------------------- /test/utils.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import "mocha"; 3 | import { SlpPaymentRequest } from ".."; 4 | import { Utils } from "../lib/utils"; 5 | 6 | describe("Utils", () => { 7 | describe("Address Conversion and Network Detection", () => { 8 | it("buildSlpUri()", () => { 9 | const expectedUri = "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk"; 10 | const uri = Utils.buildSlpUri("qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk"); 11 | assert.equal(expectedUri, uri); 12 | }); 13 | it("buildSlpUri()", () => { 14 | const expectedUri = "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk"; 15 | const uri = Utils.buildSlpUri("simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk"); 16 | assert.equal(expectedUri, uri); 17 | }); 18 | it("buildSlpUri()", () => { 19 | const expectedUri = "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk?amount=10.1"; 20 | const uri = Utils.buildSlpUri("qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk", 10.1); 21 | assert.equal(expectedUri, uri); 22 | }); 23 | it("buildSlpUri()", () => { 24 | const expectedUri = "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk?amount=1.01-fa6c74c52450fc164e17402a46645ce494a8a8e93b1383fa27460086931ef59f"; 25 | const uri = Utils.buildSlpUri("qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk", undefined, 1.01, "fa6c74c52450fc164e17402a46645ce494a8a8e93b1383fa27460086931ef59f"); 26 | assert.equal(expectedUri, uri); 27 | }); 28 | it("buildSlpUri()", () => { 29 | const expectedUri = "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk?amount=10.1&amount1=1.01-fa6c74c52450fc164e17402a46645ce494a8a8e93b1383fa27460086931ef59f"; 30 | const uri = Utils.buildSlpUri("qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk", 10.1, 1.01, "fa6c74c52450fc164e17402a46645ce494a8a8e93b1383fa27460086931ef59f"); 31 | assert.equal(expectedUri, uri); 32 | }); 33 | it("buildSlpUri()", () => { 34 | const f = () => { 35 | Utils.buildSlpUri("abc"); 36 | }; 37 | assert.throws(f, Error("Not a valid SLP address")); 38 | }); 39 | it("buildSlpUri()", () => { 40 | const f = () => { 41 | Utils.buildSlpUri("qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk", undefined, 1.01); 42 | }; 43 | assert.throws(f, Error("Missing tokenId parameter")); 44 | }); 45 | it("buildSlpUri()", () => { 46 | const f = () => { 47 | Utils.buildSlpUri("qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk", undefined, 1.01, "abc"); 48 | }; 49 | assert.throws(f, Error("TokenId is invalid, must be 32-byte hexidecimal string")); 50 | }); 51 | it("parseSlpUri()", () => { 52 | const uri = "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk"; 53 | const r = Utils.parseSlpUri(uri); 54 | const rExpected: SlpPaymentRequest = { 55 | address: "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk", 56 | }; 57 | assert.deepEqual(r, rExpected); 58 | }); 59 | it("parseSlpUri()", () => { 60 | const uri = "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk?amount=1.01&amount1=10.123-fa6c74c52450fc164e17402a46645ce494a8a8e93b1383fa27460086931ef59f"; 61 | const r = Utils.parseSlpUri(uri); 62 | const rExpected: SlpPaymentRequest = { 63 | address: "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk", 64 | amountBch: 1.01, 65 | amountToken: 10.123, 66 | tokenId: "fa6c74c52450fc164e17402a46645ce494a8a8e93b1383fa27460086931ef59f", 67 | }; 68 | assert.deepEqual(r, rExpected); 69 | }); 70 | it("parseSlpUri()", () => { 71 | const uri = "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk?amount=10.123-fa6c74c52450fc164e17402a46645ce494a8a8e93b1383fa27460086931ef59f"; 72 | const r = Utils.parseSlpUri(uri); 73 | const rExpected: SlpPaymentRequest = { 74 | address: "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk", 75 | amountToken: 10.123, 76 | tokenId: "fa6c74c52450fc164e17402a46645ce494a8a8e93b1383fa27460086931ef59f", 77 | }; 78 | assert.deepEqual(r, rExpected); 79 | }); 80 | it("parseSlpUri()", () => { 81 | const uri = "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk?amount=10.123"; 82 | const r = Utils.parseSlpUri(uri); 83 | const rExpected: SlpPaymentRequest = { 84 | address: "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk", 85 | amountBch: 10.123, 86 | }; 87 | assert.deepEqual(r, rExpected); 88 | }); 89 | it("parseSlpUri()", () => { 90 | const f = () => { 91 | const uri = "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk?amount=10.123-abch"; 92 | Utils.parseSlpUri(uri); 93 | }; 94 | assert.throws(f, Error("Token id in URI is not a valid 32-byte hexidecimal string")); 95 | }); 96 | it("parseSlpUri()", () => { 97 | const f = () => { 98 | const uri = "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk?amount=10.123-fa6c74c52450fc164e17402a46645ce494a8a8e93b1383fa27460086931ef59f-isgroup"; 99 | Utils.parseSlpUri(uri); 100 | }; 101 | assert.throws(f, Error("Token flags params not yet implemented.")); 102 | }); 103 | it("parseSlpUri()", () => { 104 | const f = () => { 105 | const uri = "simpleledger:qra3uard8aqxxc9tswlsugad9x0uglyehc74puah4w?amount=10.123-fa6c74c52450fc164e17402a46645ce494a8a8e93b1383fa27460086931ef59f"; 106 | Utils.parseSlpUri(uri); 107 | }; 108 | assert.throws(f, Error("Address is not an SLP formatted address.")); 109 | }); 110 | it("parseSlpUri()", () => { 111 | const f = () => { 112 | const uri = "simpleledger:qra3uard8aqxxc9tswlsugad9x0uglyehc74puah4w?amount=10.123?fa6c74c52450fc164e17402a46645ce494a8a8e93b1383fa27460086931ef59f-isgroup"; 113 | Utils.parseSlpUri(uri); 114 | }; 115 | assert.throws(f, Error("Cannot have character '?' more than once.")); 116 | }); 117 | it("parseSlpUri()", () => { 118 | const f = () => { 119 | const uri = "bitcoincash:qra3uard8aqxxc9tswlsugad9x0uglyehc74puah4w?amount=10.123"; 120 | Utils.parseSlpUri(uri); 121 | }; 122 | assert.throws(f, Error("Input does not start with 'simpleledger:'")); 123 | }); 124 | it("slpAddressFromHash160()", () => { 125 | const hash160 = Buffer.from("e9d42fb8b90cc1b8dcf698280bb3a066ce08876f", "hex"); 126 | const network = "mainnet"; // or "testnet" 127 | const type = "p2pkh"; // or "p2sh" 128 | const addr = Utils.slpAddressFromHash160(hash160, network, type); 129 | assert.equal(addr, "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk"); 130 | }); 131 | it("toLegacyAddress()", () => { 132 | const addr = Utils.toLegacyAddress("simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk"); 133 | assert.equal(addr, "1NKNdfgPq1EApuNaf5mrNRUPbwVHQt3MeB"); 134 | }); 135 | it("isLegacyAddress()", () => { 136 | const addr = Utils.isLegacyAddress("1NKNdfgPq1EApuNaf5mrNRUPbwVHQt3MeB"); 137 | assert.equal(addr, true); 138 | }); 139 | it("isLegacyAddress()", () => { 140 | const addr = Utils.isLegacyAddress("simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk"); 141 | assert.equal(addr, false); 142 | }); 143 | it("isLegacyAddress()", () => { 144 | const addr = Utils.isLegacyAddress("TEST"); 145 | assert.equal(addr, false); 146 | }); 147 | it("toCashAddress()", () => { 148 | const addr = Utils.toCashAddress("simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk"); 149 | assert.equal(addr, "bitcoincash:qr5agtachyxvrwxu76vzszan5pnvuzy8dumh7ynrwg"); 150 | }); 151 | it("isCashAddress()", () => { 152 | const addr = Utils.isCashAddress("bitcoincash:qr5agtachyxvrwxu76vzszan5pnvuzy8dumh7ynrwg"); 153 | assert.equal(addr, true); 154 | }); 155 | it("isCashAddress()", () => { 156 | const addr = Utils.isCashAddress("simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk"); 157 | assert.equal(addr, false); 158 | }); 159 | it("isCashAddress()", () => { 160 | const addr = Utils.isCashAddress("TEST"); 161 | assert.equal(addr, false); 162 | }); 163 | it("toSlpAddress()", () => { 164 | const addr = Utils.toSlpAddress("bitcoincash:qr5agtachyxvrwxu76vzszan5pnvuzy8dumh7ynrwg"); 165 | assert.equal(addr, "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk"); 166 | }); 167 | it("isSlpAddress()", () => { 168 | const addr = Utils.isSlpAddress("bitcoincash:qr5agtachyxvrwxu76vzszan5pnvuzy8dumh7ynrwg"); 169 | assert.equal(addr, false); 170 | }); 171 | it("isSlpAddress()", () => { 172 | const addr = Utils.isSlpAddress("simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk"); 173 | assert.equal(addr, true); 174 | }); 175 | it("isSlpAddress()", () => { 176 | const addr = Utils.isSlpAddress("TEST"); 177 | assert.equal(addr, false); 178 | }); 179 | it("isLegacyAddress()", () => { 180 | const addr = Utils.isLegacyAddress("bitcoincash:qr5agtachyxvrwxu76vzszan5pnvuzy8dumh7ynrwg"); 181 | assert.equal(addr, false); 182 | }); 183 | it("isLegacyAddress()", () => { 184 | const addr = Utils.isLegacyAddress("1NKNdfgPq1EApuNaf5mrNRUPbwVHQt3MeB"); 185 | assert.equal(addr, true); 186 | }); 187 | it("isLegacyAddress()", () => { 188 | const addr = Utils.isLegacyAddress("TEST"); 189 | assert.equal(addr, false); 190 | }); 191 | it("isMainnet()", () => { 192 | const addr = Utils.isMainnet("simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk"); 193 | assert.equal(addr, true); 194 | }); 195 | it("isMainnet()", () => { 196 | const addr = Utils.isMainnet("bitcoincash:qr5agtachyxvrwxu76vzszan5pnvuzy8dumh7ynrwg"); 197 | assert.equal(addr, true); 198 | }); 199 | it("isMainnet()", () => { 200 | const addr = Utils.isMainnet("1M57AyZWUxEA5ihv3vUcF3GrRKZqFN9vMT"); 201 | assert.equal(addr, true); 202 | }); 203 | it("isMainnet()", () => { 204 | const addr = Utils.isMainnet("qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk"); 205 | assert.equal(addr, true); 206 | }); 207 | it("isMainnet()", () => { 208 | const addr = Utils.isMainnet("qr5agtachyxvrwxu76vzszan5pnvuzy8dumh7ynrwg"); 209 | assert.equal(addr, true); 210 | }); 211 | it("isMainnet()", () => { 212 | const addr = Utils.isMainnet("slptest:qpwyc9jnwckntlpuslg7ncmhe2n423304ueqcyw80l"); 213 | assert.equal(addr, false); 214 | }); 215 | it("isMainnet()", () => { 216 | const addr = Utils.isMainnet("bchtest:qpwyc9jnwckntlpuslg7ncmhe2n423304uz5ll5saz"); 217 | assert.equal(addr, false); 218 | }); 219 | it("isMainnet()", () => { 220 | const addr = Utils.isMainnet("movycLMazxTqG3LcPGNPRaTabi8dK4eKTX"); 221 | assert.equal(addr, false); 222 | }); 223 | it("isMainnet()", () => { 224 | const addr = Utils.isMainnet("qpwyc9jnwckntlpuslg7ncmhe2n423304ueqcyw80l"); 225 | assert.equal(addr, false); 226 | }); 227 | it("isMainnet()", () => { 228 | const addr = Utils.isMainnet("qpwyc9jnwckntlpuslg7ncmhe2n423304uz5ll5saz"); 229 | assert.equal(addr, false); 230 | }); 231 | }); 232 | }); 233 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "strict": true, 5 | "target": "es5", 6 | "downlevelIteration": true, 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "lib":[ "es2017" ], 10 | "declaration": true, 11 | "sourceMap": true, 12 | "plugins": [ 13 | { "name": "typescript-tslint-plugin" } 14 | ] 15 | }, 16 | "include": [ 17 | "lib/**/*.ts" 18 | ], 19 | "exclude": [ 20 | "node_modules", 21 | "test/**/*.ts", 22 | "examples" 23 | ], 24 | "files": [ 25 | "index.ts" 26 | ] 27 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended"], 3 | "rules": { 4 | "interface-name": false, 5 | "no-console": false, 6 | "variable-name": false, 7 | "no-var-requires": false, 8 | "class-name": false, 9 | "object-literal-sort-keys": false, 10 | "no-empty": false, 11 | "trailing-comma": false 12 | } 13 | } 14 | --------------------------------------------------------------------------------