diff --git a/index.ts b/index.ts new file mode 100644 index 00000000..da95aba0 --- /dev/null +++ b/index.ts @@ -0,0 +1,105 @@ +// import * as bip39 from 'bip39'; + +const express = require("express"); +// import { ConfigService, WalletService } from './packages/cli/src/providers'; +// const axios = รง + +const app = express(); + +const port = 4000; + +// import { ConfigService, WalletService } from 'src/providers'; +// import { randomBytes } from 'crypto'; + +// import { logerror, Wallet } from 'src/common'; + +// const Wallet = require('packages/cli/src/wallet'); +import { ConfigService, WalletService } from "./dist/src/providers"; + +// const bip39 = require('bip39'); + +// const walletService = require('./packages/cli/dist/providers/'); + +// let WalletService = require("./packages/cli/dist/providers/walletService"); +// let ConfigService = require("./packages/cli/dist/providers/configService"); + +const walletHD = { + accountPath: "m/86'/0'/0'/0/0", + name: "AVF", + mnemonic: "aaa", +}; + +console.log("WalletService --- ", WalletService); +console.log("ConfigService --- ", ConfigService); + +app.use(express.json()); + +// Start the server +app.listen(port, () => { + console.log(`Server is running on port ${port}`); +}); + +// app.get("/hello", (req, res) => { +// res.status(200).json({ message: "hello" }); +// }); + +// app.get("/healthz", async (req, res) => { +// res.status(200).json({ status: "OK" }); +// }); + +app.get("/create-wallet", (req: any, res: any) => { + try { + console.log("/create-wallet START "); + + + + let configService = new ConfigService() + + const error = configService.loadCliConfig("./config.json"); + + if (error instanceof Error) { + console.warn('WARNING:', error.message); + } + + + // @ts-ignore + const walletInstance = new WalletService(configService); + console.log(" -- walletInstance ", walletInstance); + + const walletFile = walletInstance.foundWallet(); + + console.log("walletFile -- ", walletFile); + + if (walletFile !== null) { + console.log(`found an existing wallet: ${walletFile}`, new Error()); + } + + // const name = options.name + // ? options.name + // : `cat-${randomBytes(4).toString('hex')}`; + + // const wallet: Wallet = { + // accountPath: "m/86'/0'/0'/0/0", + // name: name, + // mnemonic: bip39.generateMnemonic(), + // }; + + // this.walletService.createWallet(wallet); + + // console.log('Your wallet mnemonic is: ', wallet.mnemonic); + + // console.log('exporting address to the RPC node ... '); + + // const success = await WalletService.importWallet(true); + // if (success) { + // console.log('successfully.'); + // } + } catch (error) { + // logerror('Create wallet failed!', error); + console.log("/create-wallet -- ERROR --- ", JSON.stringify(error)); + } finally { + console.log("/create-wallet END "); + } + + res.status(200).json({ message: "hello" }); +}); diff --git a/packages/cli/.eslintrc.js b/packages/cli/.eslintrc.js index 81168c91..4150d0ec 100644 --- a/packages/cli/.eslintrc.js +++ b/packages/cli/.eslintrc.js @@ -7,8 +7,8 @@ module.exports = { }, plugins: ['@typescript-eslint/eslint-plugin'], extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', + // 'plugin:@typescript-eslint/recommended', + // 'plugin:prettier/recommended', ], root: true, env: { diff --git a/packages/cli/.prettierrc b/packages/cli/.prettierrc index 3cd0ff0d..229818f6 100644 --- a/packages/cli/.prettierrc +++ b/packages/cli/.prettierrc @@ -1,4 +1,4 @@ { - "singleQuote": true, + "singleQuote": false, "trailingComma": "all" } \ No newline at end of file diff --git a/packages/cli/index.ts b/packages/cli/index.ts new file mode 100644 index 00000000..7c10a604 --- /dev/null +++ b/packages/cli/index.ts @@ -0,0 +1,533 @@ +const express = require("express"); +const app = express(); + +const port = 3333; + +import { + btc, + OpenMinterTokenInfo, + logerror, + TokenContract, +} from "./src/common"; +import { ConfigService, SpendService, WalletService } from "./src/providers"; +import { findTokenMetadataById, scaleConfig } from "./src/token"; +import Decimal from "decimal.js"; +import { estFeeSendCat20, sendCat20 } from "./src/sendCat20Handler"; +import { UTXO } from "scrypt-ts"; + +import { createRawTxSendCAT20 } from "./src/sdk"; + +const walletHD = { + accountPath: "m/86'/0'/0'/0/0", + name: "AVF", + mnemonic: "aaa", +}; + +console.log("WalletService --- ", WalletService); +console.log("ConfigService --- ", ConfigService); + +interface SendCAT20Result { + commitTxID: string + revealTxID: string + commitTxHex: string + revealTxHex: string + networkFee: number // in fb decimal +} + +interface SendCAT20Response { + errorCode: number + errorMsg: string + result: SendCAT20Result +} + + +app.use(express.json()); + +// Start the server +app.listen(port, () => { + console.log(`Server is running on port ${port}`); +}); + +app.get("/get-address", (req: any, res: any) => { + try { + let configService = new ConfigService(); + + const walletInstance = new WalletService(configService); + + const error = configService.loadCliConfig("./config.json"); + + if (error instanceof Error) { + console.warn("WARNING:", error.message); + } + + const address = walletInstance.getAddress(); + + console.log(`Your address is ${address}`); + + res.status(200).json({ result: address }); + + return; + } catch (error) { + console.log("error", error); + + res.status(403).json({ error: error.message }); + return; + } finally { + console.log("END /get-address "); + } +}); + +app.get("/create-wallet", (req: any, res: any) => { + try { + console.log("/create-wallet START "); + + let configService = new ConfigService(); + + const error = configService.loadCliConfig("./config.json"); + + if (error instanceof Error) { + console.warn("WARNING:", error.message); + } + + // @ts-ignore + const walletInstance = new WalletService(configService); + console.log(" -- walletInstance ", walletInstance); + + const walletFile = walletInstance.foundWallet(); + + console.log("walletFile -- ", walletFile); + + if (walletFile !== null) { + console.log(`found an existing wallet: ${walletFile}`, new Error()); + } + + // const name = options.name + // ? options.name + // : `cat-${randomBytes(4).toString('hex')}`; + + // const wallet: Wallet = { + // accountPath: "m/86'/0'/0'/0/0", + // name: name, + // mnemonic: bip39.generateMnemonic(), + // }; + + // this.walletService.createWallet(wallet); + + // console.log('Your wallet mnemonic is: ', wallet.mnemonic); + + // console.log('exporting address to the RPC node ... '); + + // const success = await WalletService.importWallet(true); + // if (success) { + // console.log('successfully.'); + // } + } catch (error) { + // logerror('Create wallet failed!', error); + console.log("/create-wallet -- ERROR --- ", JSON.stringify(error)); + } finally { + console.log("/create-wallet END "); + } + + res.status(200).json({ message: "hello" }); +}); + +app.post("/name", (req: any, res: any) => { + const { name, age } = req.body; + console.log("name", name); + console.log("age", age); + + res.status(200).json({ name: name, age: age }); + return; +}); + + +app.post("/send-cat20", async (req: any, res: any) => { + try { + console.log("req.body: ", req.body); + // Get Body + const { + privateKey, + receiver: receiverAddress, + amount, + tokenId, + utxos, + feeRate, + withdrawUUID, + isBroadcast = false, + } = req.body as { + privateKey: string; + receiver: string; + amount: string; + tokenId: string; + utxos: UTXO[]; + feeRate: number; + withdrawUUID: string; + isBroadcast?: boolean; + }; + + console.log({ tokenId }); + console.log({ amount }); + console.log({ receiverAddress }); + console.log({ feeRate }); + + console.log("/send START "); + + + if (!tokenId) { + return createErrorUnknown(res, `tokenId empty.`); + } + if (!amount) { + return createErrorUnknown(res, `amount empty.`); + } + if (!receiverAddress) { + return createErrorUnknown(res, `receiver empty.`); + } + if (!feeRate) { + return createErrorUnknown(res, `feeRate empty.`); + } + + let configService = new ConfigService(); + const error = configService.loadCliConfig("./config.json"); + if (error instanceof Error) { + console.warn("WARNING:", error.message); + } + + const spendService = new SpendService(configService); + const walletService = new WalletService(configService); + + walletService.overwriteWallet(privateKey); + console.log(" -- overwriteWallet "); + console.log("New wallet address: ", walletService.getAddress()); + + // find token id + const senderAddress = walletService.getAddress(); + const token = await findTokenMetadataById(configService, tokenId); + + if (!token) { + return createErrorUnknown(res, `Token not found: ${tokenId}`); + } + + let receiver: btc.Address; + + try { + receiver = btc.Address.fromString(receiverAddress); + + if (receiver.type !== "taproot") { + return createErrorUnknown(res, `Invalid address type: ${receiver.type}`) + } + } catch (error) { + return createErrorUnknown(res, `Invalid receiver address: "${receiverAddress}" - err: ${error}`) + } + + const resp = await sendCat20( + token, + receiver, + amount, + senderAddress, + configService, + walletService, + spendService, + utxos, + isBroadcast, + feeRate, + ); + + if (resp.errorCode) { + // save logs 1 + try { + saveLogs(withdrawUUID, senderAddress.toString(), receiver.toString(), token.tokenId, token.info.symbol, amount.toString(), res, resp) + } catch (error) { + console.log("saveLogs1 er", error) + } + return handleErrorResponse(res, resp.errorCode, resp.errorMsg) + } + + const result = resp.result + + const networkFee = result.commitTx.getFee() + result.revealTx.getFee(); + + console.log("result.commitTx.id", result.commitTx.id); + console.log("result.revealTx.id", result.revealTx.id); + console.log("Total network fee: ", networkFee); + + + const dataResp: SendCAT20Result = { + commitTxID: result.commitTx.id, + commitTxHex: result.commitTx.uncheckedSerialize(), + revealTxID: result.revealTx.id, + revealTxHex: result.revealTx.uncheckedSerialize(), + networkFee: networkFee, + } + + let logResp: SendCAT20Response = { + errorCode: 0, + errorMsg: "", + result: dataResp, + } + + // save logs 2: + try { + saveLogs(withdrawUUID, senderAddress.toString(), receiver.toString(), token.tokenId, token.info.symbol, amount.toString(), res, logResp) + } catch (error) { + console.log("saveLogs2 er", error) + } + + return handleResponseOK(res, dataResp) + + } catch (error) { + console.log("/send -- ERROR --- ", error); + // res.status(500).json({ error: error.message || error, message: "Insufficient balance" }); + return handleErrorResponse(res, '-9999', error) + } finally { + console.log("/send END "); + } +}); + + +app.post("/est-fee-send-cat20", async (req: any, res: any) => { + try { + // Get Body + const { + privateKey, + amount, + tokenId, + } = req.body as { + privateKey: string; + amount: string; + tokenId: string; + }; + + console.log({ tokenId }); + console.log({ amount }); + + console.log("/est-fee-send-cat20 START "); + + let configService = new ConfigService(); + const error = configService.loadCliConfig("./config.json"); + if (error instanceof Error) { + console.warn("WARNING:", error.message); + } + + const spendService = new SpendService(configService); + const walletService = new WalletService(configService); + + walletService.overwriteWallet(privateKey); + console.log(" -- overwriteWallet "); + console.log("New wallet address: ", walletService.getAddress()); + + // find token id + const senderAddress = walletService.getAddress(); + const token = await findTokenMetadataById(configService, tokenId); + + if (!token) { + return handleError(res, `Token not found: ${tokenId}`); + } + + const result = await estFeeSendCat20( + token, + amount, + senderAddress, + configService, + spendService, + ); + + console.log("estFeeSendCat20 result ", result); + + res.status(200).json({ + pickedUTXOs: result, + }); + + } catch (error) { + console.log("/est-fee-send-cat20 -- ERROR --- ", error); + res.status(500).json({ error: error }); + } finally { + console.log("/est-fee-send-cat20 END "); + } +}); + + +app.post("/create-tx-send-cat20", async (req: any, res: any) => { + try { + console.log("create-tx-send-cat20 req.body: ", req.body); + // Get Body + const { + senderAddress, + senderPubKey, + receiverAddress, + amount, + tokenId, + utxos, + feeRate, + } = req.body as { + senderAddress: string; + senderPubKey: string; // internal pub key 33 bytes - hex encode + receiverAddress: string; + amount: string; + tokenId: string; + utxos: UTXO[]; + feeRate: number; + }; + + console.log("/create-tx-send-cat20 req.body: ", req.body); + console.log({ tokenId }); + console.log({ amount }); + console.log({ receiverAddress }); + console.log({ feeRate }); + + console.log("/create-tx-send-cat20 START "); + + let configService = new ConfigService(); + const error = configService.loadCliConfig("./config.json"); + if (error instanceof Error) { + console.warn("WARNING:", error.message); + } + + const spendService = new SpendService(configService); + const walletService = new WalletService(configService); + + const senderPubKeyBytes = Buffer.from(senderPubKey, "hex"); + walletService.overwriteWalletByAddress(senderAddress, senderPubKeyBytes); + // console.log(" -- overwriteWallet "); + // console.log("New wallet address: ", walletService.getAddress()); + + // find token id + // const senderAddress = walletService.getAddress(); + const token = await findTokenMetadataById(configService, tokenId); + + if (!token) { + return handleError(res, `create-tx-send-cat20 Token not found: ${tokenId}`); + } + + let receiver: btc.Address; + + try { + receiver = btc.Address.fromString(receiverAddress); + + if (receiver.type !== "taproot") { + return handleError(res, `Invalid address type: ${receiver.type}`); + } + } catch (error) { + return handleError( + res, + `Invalid receiver address: "${receiverAddress}" - err: ${error}`, + ); + } + + const result = await createRawTxSendCAT20({ + senderAddress, + receiverAddress, + amount: BigInt(amount), + token, + configService, + spendService, + walletService, + feeUtxos: utxos, + feeRate + }); + // token, + // receiver, + // amount, + // senderAddress, + // configService, + // walletService, + // spendService, + // utxos, + // feeRate, + + + if (!result) { + return handleError(res, `create-tx-send-cat20 failed!`); + } + + // const networkFee = result.commitTx.getFee() + result.revealTx.getFee(); + + console.log("result.commitTx.id", result.commitTx.psbtBase64); + console.log("result.revealTx.id", result.revealTx.psbtBase64); + // console.log("Total network fee: ", networkFee); + + res.status(200).json(result); + + } catch (error) { + console.log("/create-tx-send-cat20 -- ERROR --- ", error); + console.log("/create-tx-send-cat20 -- ERROR --- ", JSON.stringify(error) || error); + res.status(500).json({ error: "Send transaction failed!" }); + } finally { + console.log("/create-tx-send-cat20 END "); + } +}); + + + +const errorCodes = { + '-1000': "Insufficient satoshis balance!", + '-1001': "Insufficient token balance!", + '-1002': "Merge token failed!" +}; + +function handleErrorResponse(res: any, errorCode, errorMsgInput = "") { + const errorMsg = errorCodes[errorCode] || errorMsgInput; + res.status(500).json({ + errorCode: parseInt(errorCode), + errorMsg: errorMsg, + result: null + }); +} + +function createErrorUnknown(res: any, errorMsg) { + res.status(500).json({ + errorCode: parseInt('-9999'), + errorMsg: errorMsg, + result: null + }); +} + +function handleResponseOK(res: any, result = {}) { + res.status(200).json({ + errorCode: 0, + errorMsg: "", + result: result + }); +} + +function handleError(res: any, message: string) { + console.error(message); + res.status(500).json({ error: message }); +} + +// save log: +const axios = require('axios'); +function saveLogs(withdrawUUID, senderAddress, receivedAddresses, tokenID, symbol, amount, reqs, resps) { + + try { + let data = JSON.stringify({ + "withdrawUUID": withdrawUUID, + "senderAddress": senderAddress, + "tokenID": tokenID, + "symbol": symbol, + "amount": amount, + "receivedAddresses": receivedAddresses, + "reqs": reqs, + "resps": resps, + }); + + let config = { + method: 'post', + maxBodyLength: Infinity, + url: 'https://fractal-bridges-api.trustless.computer/api/cat20/internal/add-withdraw-logs', + headers: { + 'Content-Type': 'application/json' + }, + data: data + }; + + axios.request(config) + .then((response) => { + console.log(JSON.stringify(response.data)); + }) + .catch((error) => { + console.log("function saveLogs error1", error); + }); + } catch (error) { + console.log("function saveLogs error2", error); + } +} \ No newline at end of file diff --git a/packages/cli/package.json b/packages/cli/package.json index ceb99290..9d0a8f91 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -9,6 +9,7 @@ "cat-cli": "bin/cat.js" }, "scripts": { + "start": "ts-node index.ts", "build": "nest build", "prepublishOnly": "yarn build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", @@ -85,4 +86,4 @@ "coverageDirectory": "../coverage", "testEnvironment": "node" } -} +} \ No newline at end of file diff --git a/packages/cli/src/commands/base.command.ts b/packages/cli/src/commands/base.command.ts index 5d734d76..9e0344b0 100644 --- a/packages/cli/src/commands/base.command.ts +++ b/packages/cli/src/commands/base.command.ts @@ -1,9 +1,9 @@ -import { accessSync, constants } from 'fs'; -import { Option, CommandRunner } from 'nest-commander'; -import { CliConfig, logerror, resolveConfigPath } from 'src/common'; -import { WalletService } from 'src/providers'; -import { ConfigService } from 'src/providers/configService'; -import { URL } from 'url'; +import { accessSync, constants } from "fs"; +import { Option, CommandRunner } from "nest-commander"; +import { CliConfig, logerror, resolveConfigPath } from "../common"; +import { WalletService } from "../providers"; +import { ConfigService } from "../providers/configService"; +import { URL } from "url"; export interface BaseCommandOptions { config?: string; network?: string; @@ -32,12 +32,12 @@ export abstract class BaseCommand extends CommandRunner { passedParams: string[], options?: BaseCommandOptions, ): Promise { - const configPath = resolveConfigPath(options?.config || ''); + const configPath = resolveConfigPath(options?.config || ""); const error = this.configService.loadCliConfig(configPath); if (error instanceof Error) { - console.warn('WARNING:', error.message); + console.warn("WARNING:", error.message); } const cliConfig = this.takeConfig(options); @@ -118,8 +118,8 @@ export abstract class BaseCommand extends CommandRunner { } @Option({ - flags: '-c, --config [config file]', - description: 'Special a config file', + flags: "-c, --config [config file]", + description: "Special a config file", }) parseConfig(val: string): string { const configPath = resolveConfigPath(val); @@ -133,23 +133,23 @@ export abstract class BaseCommand extends CommandRunner { } @Option({ - flags: '-d, --datadir [datadir]', - description: 'Special a data dir', + flags: "-d, --datadir [datadir]", + description: "Special a data dir", }) parseDataDir(val: string): string { return val; } @Option({ - flags: '-n, --network [network]', - description: 'Special a network', - choices: ['fractal-mainnet', 'fractal-testnet', 'btc-signet'], + flags: "-n, --network [network]", + description: "Special a network", + choices: ["fractal-mainnet", "fractal-testnet", "btc-signet"], }) parseNetwork(val: string): string { if ( - val === 'fractal-mainnet' || - val === 'fractal-testnet' || - val === 'btc-signet' + val === "fractal-mainnet" || + val === "fractal-testnet" || + val === "btc-signet" ) { return val; } @@ -157,8 +157,8 @@ export abstract class BaseCommand extends CommandRunner { } @Option({ - flags: '-t, --tracker [tracker]', - description: 'Special a tracker URL', + flags: "-t, --tracker [tracker]", + description: "Special a tracker URL", }) parseTracker(val: string): string { try { @@ -170,8 +170,8 @@ export abstract class BaseCommand extends CommandRunner { } @Option({ - flags: '--rpc-url [rpcurl]', - description: 'Special a rpc URL', + flags: "--rpc-url [rpcurl]", + description: "Special a rpc URL", }) parseRpcUrl(val: string): string { try { @@ -183,16 +183,16 @@ export abstract class BaseCommand extends CommandRunner { } @Option({ - flags: '--rpc-username [rpcusername]', - description: 'Special a rpc username', + flags: "--rpc-username [rpcusername]", + description: "Special a rpc username", }) parseRpcUsername(val: string): string { return val; } @Option({ - flags: '--rpc-password [rpcpassword]', - description: 'Special a rpc password', + flags: "--rpc-password [rpcpassword]", + description: "Special a rpc password", }) parseRpcPassword(val: string): string { return val; diff --git a/packages/cli/src/commands/boardcast.command.ts b/packages/cli/src/commands/boardcast.command.ts index 096fcb0f..4c7e9331 100644 --- a/packages/cli/src/commands/boardcast.command.ts +++ b/packages/cli/src/commands/boardcast.command.ts @@ -1,7 +1,7 @@ import { Option } from 'nest-commander'; import { BaseCommand, BaseCommandOptions } from './base.command'; -import { ConfigService, SpendService, WalletService } from 'src/providers'; -import { CliConfig, getFeeRate } from 'src/common'; +import { ConfigService, SpendService, WalletService } from '../providers'; +import { CliConfig, getFeeRate } from '../common'; export interface BoardcastCommandOptions extends BaseCommandOptions { maxFeeRate?: number; diff --git a/packages/cli/src/commands/deploy/deploy.command.ts b/packages/cli/src/commands/deploy/deploy.command.ts index 37b0bfc8..5ad34f12 100644 --- a/packages/cli/src/commands/deploy/deploy.command.ts +++ b/packages/cli/src/commands/deploy/deploy.command.ts @@ -1,4 +1,4 @@ -import { Command, Option } from 'nest-commander'; +import { Command, Option } from "nest-commander"; import { MinterType, getUtxos, @@ -11,15 +11,15 @@ import { checkTokenInfo, btc, scaleByDecimals, -} from 'src/common'; -import { deploy, getMinterInitialTxState } from './ft'; -import { ConfigService } from 'src/providers/configService'; -import { SpendService, WalletService } from 'src/providers'; -import { Inject } from '@nestjs/common'; -import { addTokenMetadata } from 'src/token'; -import { openMint } from '../mint/ft.open-minter'; -import { isAbsolute, join } from 'path'; -import { accessSync, constants, readFileSync } from 'fs'; +} from "../../common"; +import { deploy, getMinterInitialTxState } from "./ft"; +import { ConfigService } from "../../providers/configService"; +import { SpendService, WalletService } from "../../providers"; +import { Inject } from "@nestjs/common"; +import { addTokenMetadata } from "../../token"; +import { openMint } from "../mint/ft.open-minter"; +import { isAbsolute, join } from "path"; +import { accessSync, constants, readFileSync } from "fs"; import { BoardcastCommand, BoardcastCommandOptions, @@ -53,8 +53,8 @@ function isEmptyOption(options: DeployCommandOptions) { } @Command({ - name: 'deploy', - description: 'Deploy an open-mint fungible token (FT)', + name: "deploy", + description: "Deploy an open-mint fungible token (FT)", }) export class DeployCommand extends BoardcastCommand { constructor( @@ -82,8 +82,8 @@ export class DeployCommand extends BoardcastCommand { if (isEmptyOption(options)) { logerror( - 'Should deploy with `--metadata=your.json` or with options like `--name=cat --symbol=cat --decimals=0 --max=21000000 --premine=0 --limit=1000` ', - new Error('No metadata found'), + "Should deploy with `--metadata=your.json` or with options like `--name=cat --symbol=cat --decimals=0 --max=21000000 --premine=0 --limit=1000` ", + new Error("No metadata found"), ); return; } @@ -91,7 +91,7 @@ export class DeployCommand extends BoardcastCommand { const err = checkTokenInfo(info); if (err instanceof Error) { - logerror('Invalid token metadata!', err); + logerror("Invalid token metadata!", err); return; } @@ -104,7 +104,7 @@ export class DeployCommand extends BoardcastCommand { ); if (utxos.length === 0) { - console.warn('Insufficient satoshi balance!'); + console.warn("Insufficient satoshi balance!"); return; } @@ -196,31 +196,31 @@ export class DeployCommand extends BoardcastCommand { } } } catch (error) { - logerror('Deploy failed!', error); + logerror("Deploy failed!", error); } } @Option({ - flags: '-n, --name [name]', - name: 'name', - description: 'token name', + flags: "-n, --name [name]", + name: "name", + description: "token name", }) parseName(val: string): string { if (!val) { - logerror("Name can't be empty!", new Error('Empty symbol')); + logerror("Name can't be empty!", new Error("Empty symbol")); process.exit(0); } return val; } @Option({ - flags: '-s, --symbol [symbol]', - name: 'symbol', - description: 'token symbol', + flags: "-s, --symbol [symbol]", + name: "symbol", + description: "token symbol", }) parseSymbol(val: string): string { if (!val) { - logerror("Symbol can't be empty!", new Error('Empty symbol')); + logerror("Symbol can't be empty!", new Error("Empty symbol")); process.exit(0); } @@ -228,9 +228,9 @@ export class DeployCommand extends BoardcastCommand { } @Option({ - flags: '-d, --decimals [decimals]', - name: 'decimals', - description: 'token decimals', + flags: "-d, --decimals [decimals]", + name: "decimals", + description: "token decimals", }) parseDecimals(val: string): number { if (!val) { @@ -240,19 +240,19 @@ export class DeployCommand extends BoardcastCommand { try { const decimals = parseInt(val); if (isNaN(decimals)) { - logwarn('Invalid decimals, use defaut 0', new Error()); + logwarn("Invalid decimals, use defaut 0", new Error()); } return decimals; } catch (error) { - logwarn('Invalid decimals, use defaut 0', error); + logwarn("Invalid decimals, use defaut 0", error); } return 0; } @Option({ - flags: '-l, --limit [limit]', - name: 'limit', - description: 'limit of per mint', + flags: "-l, --limit [limit]", + name: "limit", + description: "limit of per mint", }) parseLimit(val: string): bigint { if (!val) { @@ -262,33 +262,33 @@ export class DeployCommand extends BoardcastCommand { try { return BigInt(val); } catch (error) { - logwarn('Invalid limit, use defaut 1000n', error); + logwarn("Invalid limit, use defaut 1000n", error); } return BigInt(1000); } @Option({ - flags: '-m, --max [max]', - name: 'max', - description: 'token max supply', + flags: "-m, --max [max]", + name: "max", + description: "token max supply", }) parseMax(val: string): bigint { if (!val) { - logerror('Invalid token max supply!', new Error('Empty max supply')); + logerror("Invalid token max supply!", new Error("Empty max supply")); process.exit(0); } try { return BigInt(val); } catch (error) { - logerror('Invalid token max supply!', error); + logerror("Invalid token max supply!", error); process.exit(0); } } @Option({ - flags: '-p, --premine [premine]', - name: 'premine', - description: 'token premine', + flags: "-p, --premine [premine]", + name: "premine", + description: "token premine", }) parsePremine(val: string): bigint { if (!val) { @@ -297,15 +297,15 @@ export class DeployCommand extends BoardcastCommand { try { return BigInt(val); } catch (error) { - logerror('Invalid token premine!', error); + logerror("Invalid token premine!", error); process.exit(0); } } @Option({ - flags: '-m, --metadata [metadata]', - name: 'metadata', - description: 'token metadata', + flags: "-m, --metadata [metadata]", + name: "metadata", + description: "token metadata", }) parseMetadata(val: string): string { if (!val) { diff --git a/packages/cli/src/commands/deploy/ft.ts b/packages/cli/src/commands/deploy/ft.ts index 5ecd00c8..d84cc7b2 100644 --- a/packages/cli/src/commands/deploy/ft.ts +++ b/packages/cli/src/commands/deploy/ft.ts @@ -1,5 +1,5 @@ /* eslint-disable prettier/prettier */ -import { UTXO, toByteString } from 'scrypt-ts'; +import { UTXO, toByteString } from "scrypt-ts"; import { broadcast, getTokenContractP2TR, diff --git a/packages/cli/src/commands/mint/mint.command.ts b/packages/cli/src/commands/mint/mint.command.ts index aaceed63..3ea02486 100644 --- a/packages/cli/src/commands/mint/mint.command.ts +++ b/packages/cli/src/commands/mint/mint.command.ts @@ -1,4 +1,4 @@ -import { Command, Option } from 'nest-commander'; +import { Command, Option } from "nest-commander"; import { getUtxos, OpenMinterTokenInfo, @@ -23,10 +23,10 @@ import Decimal from 'decimal.js'; import { BoardcastCommand, BoardcastCommandOptions, -} from '../boardcast.command'; -import { broadcastMergeTokenTxs, mergeTokens } from '../send/merge'; -import { calcTotalAmount, sendToken } from '../send/ft'; -import { pickLargeFeeUtxo } from '../send/pick'; +} from "../boardcast.command"; +import { broadcastMergeTokenTxs, mergeTokens } from "../send/merge"; +import { calcTotalAmount, sendToken } from "../send/ft"; +import { pickLargeFeeUtxo } from "../send/pick"; interface MintCommandOptions extends BoardcastCommandOptions { id: string; new?: number; @@ -37,8 +37,8 @@ function getRandomInt(max: number) { } @Command({ - name: 'mint', - description: 'Mint a token', + name: "mint", + description: "Mint a token", }) export class MintCommand extends BoardcastCommand { constructor( @@ -89,7 +89,7 @@ export class MintCommand extends BoardcastCommand { const feeRate = await this.getFeeRate(); const feeUtxos = await this.getFeeUTXOs(address); if (feeUtxos.length === 0) { - console.warn('Insufficient satoshis balance!'); + console.warn("Insufficient satoshis balance!"); return; } @@ -101,7 +101,7 @@ export class MintCommand extends BoardcastCommand { const maxTry = count < MAX_RETRY_COUNT ? count : MAX_RETRY_COUNT; if (count == 0 && index >= maxTry) { - console.error('No available minter UTXO found!'); + console.error("No available minter UTXO found!"); return; } @@ -120,14 +120,14 @@ export class MintCommand extends BoardcastCommand { if (isOpenMinter(token.info.minterMd5)) { const minterState = minter.state.data; if (minterState.isPremined && amount > scaledInfo.limit) { - console.error('The number of minted tokens exceeds the limit!'); + console.error("The number of minted tokens exceeds the limit!"); return; } const limit = scaledInfo.limit; if (!minter.state.data.isPremined && scaledInfo.premine > 0n) { - if (typeof amount === 'bigint') { + if (typeof amount === "bigint") { if (amount !== scaledInfo.premine) { throw new Error( `first mint amount should equal to premine ${scaledInfo.premine}`, @@ -197,16 +197,16 @@ export class MintCommand extends BoardcastCommand { ); return; } else { - throw new Error('unkown minter!'); + throw new Error("unkown minter!"); } } console.error(`mint token [${token.info.symbol}] failed`); } else { - throw new Error('expect a ID option'); + throw new Error("expect a ID option"); } } catch (error) { - logerror('mint failed!', error); + logerror("mint failed!", error); } } @@ -241,7 +241,7 @@ export class MintCommand extends BoardcastCommand { ); if (e instanceof Error) { - logerror('merge token failed!', e); + logerror("merge token failed!", e); return; } @@ -279,8 +279,8 @@ export class MintCommand extends BoardcastCommand { } @Option({ - flags: '-i, --id [tokenId]', - description: 'ID of the token', + flags: "-i, --id [tokenId]", + description: "ID of the token", }) parseId(val: string): string { return val; @@ -298,7 +298,7 @@ export class MintCommand extends BoardcastCommand { }); if (feeUtxos.length === 0) { - console.warn('Insufficient satoshis balance!'); + console.warn("Insufficient satoshis balance!"); return []; } return feeUtxos; diff --git a/packages/cli/src/commands/send/ft.ts b/packages/cli/src/commands/send/ft.ts index 97cdc39a..3c66e7a2 100644 --- a/packages/cli/src/commands/send/ft.ts +++ b/packages/cli/src/commands/send/ft.ts @@ -20,7 +20,7 @@ import { logerror, btc, verifyContract, -} from 'src/common'; +} from "../../common"; import { int2ByteString, MethodCallOptions, @@ -28,7 +28,7 @@ import { PubKey, UTXO, fill, -} from 'scrypt-ts'; +} from "scrypt-ts"; import { emptyTokenAmountArray, emptyTokenArray, @@ -47,8 +47,8 @@ import { ChangeInfo, MAX_TOKEN_OUTPUT, MAX_INPUT, -} from '@cat-protocol/cat-smartcontracts'; -import { ConfigService, WalletService } from 'src/providers'; +} from "@cat-protocol/cat-smartcontracts"; +import { ConfigService, WalletService } from "../../providers"; async function unlockToken( wallet: WalletService, @@ -73,12 +73,13 @@ async function unlockToken( sighash.hash, ); const pubkeyX = wallet.getXOnlyPublicKey(); + console.log("unlockToken ", { pubkeyX }); const pubKeyPrefix = wallet.getPubKeyPrefix(); const tokenUnlockArgs: TokenUnlockArgs = { isUserSpend: true, userPubKeyPrefix: pubKeyPrefix, userPubKey: PubKey(pubkeyX), - userSig: sig.toString('hex'), + userSig: sig.toString("hex"), contractInputIndex: 0n, }; @@ -117,7 +118,7 @@ async function unlockToken( ...callToBufferList(tokenCall), // taproot script + cblock token.lockingScript.toBuffer(), - Buffer.from(cblockToken, 'hex'), + Buffer.from(cblockToken, "hex"), ]; revealTx.inputs[tokenInputIndex].witnesses = witnesses; @@ -128,8 +129,8 @@ async function unlockToken( tokenInputIndex, witnesses, ); - if (typeof res === 'string') { - console.error('unlocking token contract failed!', res); + if (typeof res === "string") { + console.error("unlocking token contract failed!", res); return false; } return true; @@ -199,7 +200,7 @@ async function unlockGuard( ...callToBufferList(transferGuardCall), // taproot script + cblock transferGuard.lockingScript.toBuffer(), - Buffer.from(transferCblock, 'hex'), + Buffer.from(transferCblock, "hex"), ]; revealTx.inputs[guardInputIndex].witnesses = witnesses; @@ -210,8 +211,8 @@ async function unlockGuard( guardInputIndex, witnesses, ); - if (typeof res === 'string') { - console.error('unlocking guard contract failed!', res); + if (typeof res === "string") { + console.error("unlocking guard contract failed!", res); return false; } return true; @@ -257,11 +258,17 @@ export function createGuardContract( .change(changeAddress); if (commitTx.getChangeOutput() === null) { - console.error('Insufficient satoshis balance!'); - return null; + console.info("Insufficient satoshis balance!"); + throw new Error("Insufficient satoshis balance!"); + // return null; } commitTx.outputs[2].satoshis -= 1; - wallet.signTx(commitTx); + if (wallet.getWallet().address === "") { + wallet.signTx(commitTx); + } else { + // not sign + } + const contact: GuardContract = { utxo: { @@ -313,6 +320,9 @@ export async function sendToken( changeAddress, ); + + console.log("sendToken ", { commitResult }); + if (commitResult === null) { return null; } @@ -360,9 +370,12 @@ export async function sendToken( ]; if (inputUtxos.length > MAX_INPUT) { - throw new Error('to much input'); + throw new Error("to much input"); } + console.log("sendToken before reveal"); + + const revealTx = new btc.Transaction() .from(inputUtxos) .addOutput( @@ -379,6 +392,10 @@ export async function sendToken( ) .feePerByte(feeRate); + + console.log("sendToken reveal 1: ", { revealTx }); + + if (changeTokenState) { revealTx.addOutput( new btc.Transaction.Output({ @@ -388,6 +405,10 @@ export async function sendToken( ); } + + console.log("sendToken reveal 2: ", { revealTx }); + + const satoshiChangeScript = btc.Script.fromAddress(changeAddress); revealTx.addOutput( new btc.Transaction.Output({ @@ -396,6 +417,9 @@ export async function sendToken( }), ); + + console.log("sendToken reveal 3: ", { revealTx }); + const tokenTxs = await Promise.all( tokens.map(async ({ utxo: tokenUtxo }) => { let prevTx: btc.Transaction | null = null; @@ -444,7 +468,7 @@ export async function sendToken( let prevPrevTx: btc.Transaction | null = null; const prevPrevTxId = - prevTx.inputs[prevTokenInputIndex].prevTxId.toString('hex'); + prevTx.inputs[prevTokenInputIndex].prevTxId.toString("hex"); if (cachedTxs.has(prevPrevTxId)) { prevPrevTx = cachedTxs.get(prevPrevTxId); @@ -518,8 +542,9 @@ export async function sendToken( (changeTokenState === null ? 0 : Postage.TOKEN_POSTAGE); if (satoshiChangeAmount <= CHANGE_MIN_POSTAGE) { - console.error('Insufficient satoshis balance!'); - return null; + console.info("Insufficient satoshis balance!"); + throw new Error("Insufficient satoshis balance!"); + // return null; } const satoshiChangeOutputIndex = changeTokenState === null ? 2 : 3; @@ -531,8 +556,8 @@ export async function sendToken( revealTx, tokens.map((_, i) => i).concat([tokens.length]), [ - ...new Array(tokens.length).fill(Buffer.from(tokenTapScript, 'hex')), - Buffer.from(guardTapScript, 'hex'), + ...new Array(tokens.length).fill(Buffer.from(tokenTapScript, "hex")), + Buffer.from(guardTapScript, "hex"), ], ); @@ -581,7 +606,11 @@ export async function sendToken( return null; } - wallet.signTx(revealTx); + if (wallet.getWallet().address === "") { + wallet.signTx(revealTx); + } else { + // not sign + } const receiverTokenContract: TokenContract = { utxo: { @@ -645,8 +674,8 @@ const calcVsize = async ( revealTx, tokens.map((_, i) => i).concat([tokens.length]), [ - ...new Array(tokens.length).fill(Buffer.from(tokenTapScript, 'hex')), - Buffer.from(guardTapScript, 'hex'), + ...new Array(tokens.length).fill(Buffer.from(tokenTapScript, "hex")), + Buffer.from(guardTapScript, "hex"), ], ); @@ -686,8 +715,10 @@ const calcVsize = async ( ); wallet.signTx(revealTx); + const vsize = revealTx.vsize; resetTx(revealTx); + return vsize; }; diff --git a/packages/cli/src/commands/send/merge.ts b/packages/cli/src/commands/send/merge.ts index 5e006a4c..69357811 100644 --- a/packages/cli/src/commands/send/merge.ts +++ b/packages/cli/src/commands/send/merge.ts @@ -6,10 +6,10 @@ import { log, TokenMetadata, TokenContract, -} from 'src/common'; -import { ConfigService, SpendService, WalletService } from 'src/providers'; -import { UTXO } from 'scrypt-ts'; -import { calcTotalAmount, sendToken } from './ft'; +} from "../../common"; +import { ConfigService, SpendService, WalletService } from "../../providers"; +import { UTXO } from "scrypt-ts"; +import { calcTotalAmount, sendToken } from "./ft"; async function feeSplitTx( configService: ConfigService, @@ -40,7 +40,12 @@ async function feeSplitTx( ); } _splitFeeTx.feePerByte(feeRate); - walletService.signTx(_splitFeeTx); + if (walletService.getWallet().address === "") { + walletService.signTx(_splitFeeTx); + } else { + // TODO 2525 + } + return _splitFeeTx.vsize; } @@ -173,7 +178,7 @@ export async function mergeTokens( newToken = result.contracts[0]; batchTokensTobeMerge.splice(0, 4, ...result.contracts); } else { - return [tokens, feeUtxos, new Error('merge tokens failed!')]; + return [tokens, feeUtxos, new Error("merge tokens failed!")]; } } @@ -246,7 +251,7 @@ export async function waitTxConfirm( } } -export const MERGE_TOKEN_FAILED_ERR = 'broadcast merge token txs failed'; +export const MERGE_TOKEN_FAILED_ERR = "broadcast merge token txs failed"; export function isMergeTxFail(e: Error) { return e.message.includes(MERGE_TOKEN_FAILED_ERR); diff --git a/packages/cli/src/commands/send/pick.ts b/packages/cli/src/commands/send/pick.ts index 774281d9..e54e2775 100644 --- a/packages/cli/src/commands/send/pick.ts +++ b/packages/cli/src/commands/send/pick.ts @@ -1,5 +1,5 @@ -import { TokenContract } from 'src/common'; -import { UTXO } from 'scrypt-ts'; +import { TokenContract } from "../../common"; +import { UTXO } from "scrypt-ts"; export function pick( tokens: Array, diff --git a/packages/cli/src/commands/send/send.command.ts b/packages/cli/src/commands/send/send.command.ts index 1139fabc..63fffa69 100644 --- a/packages/cli/src/commands/send/send.command.ts +++ b/packages/cli/src/commands/send/send.command.ts @@ -1,4 +1,4 @@ -import { Command, InquirerService, Option } from 'nest-commander'; +import { Command, InquirerService, Option } from "nest-commander"; import { getUtxos, getTokens, @@ -10,19 +10,19 @@ import { sleep, btc, unScaleByDecimals, -} from 'src/common'; -import { sendToken } from './ft'; -import { pick, pickLargeFeeUtxo } from './pick'; -import { ConfigService, SpendService, WalletService } from 'src/providers'; -import { Inject } from '@nestjs/common'; -import { RetrySendQuestionAnswers } from 'src/questions/retry-send.question'; -import { findTokenMetadataById, scaleConfig } from 'src/token'; -import Decimal from 'decimal.js'; +} from "../../common"; +import { sendToken } from "./ft"; +import { pick, pickLargeFeeUtxo } from "./pick"; +import { ConfigService, SpendService, WalletService } from "../../providers"; +import { Inject } from "@nestjs/common"; +import { RetrySendQuestionAnswers } from "../../questions/retry-send.question"; +import { findTokenMetadataById, scaleConfig } from "../../token"; +import Decimal from "decimal.js"; import { BoardcastCommand, BoardcastCommandOptions, -} from '../boardcast.command'; -import { isMergeTxFail, mergeTokens } from './merge'; +} from "../boardcast.command"; +import { isMergeTxFail, mergeTokens } from "./merge"; interface SendCommandOptions extends BoardcastCommandOptions { id: string; @@ -32,8 +32,8 @@ interface SendCommandOptions extends BoardcastCommandOptions { } @Command({ - name: 'send', - description: 'Send tokens', + name: "send", + description: "Send tokens", }) export class SendCommand extends BoardcastCommand { constructor( @@ -49,7 +49,7 @@ export class SendCommand extends BoardcastCommand { options?: SendCommandOptions, ): Promise { if (!options.id) { - logerror('expect a tokenId option', new Error()); + logerror("expect a tokenId option", new Error()); return; } try { @@ -65,7 +65,7 @@ export class SendCommand extends BoardcastCommand { try { receiver = btc.Address.fromString(inputs[0]); - if (receiver.type !== 'taproot') { + if (receiver.type !== "taproot") { console.error(`Invalid address type: ${receiver.type}`); return; } @@ -100,11 +100,11 @@ export class SendCommand extends BoardcastCommand { if (needRetry(error)) { // if send token failed, we request to retry const { retry } = await this.inquirer.ask( - 'retry_send_question', + "retry_send_question", {}, ); - if (retry === 'abort') { + if (retry === "abort") { return; } console.warn(`retry to send token [${token.info.symbol}] ...`); @@ -123,8 +123,9 @@ export class SendCommand extends BoardcastCommand { receiver: btc.Address, amount: bigint, address: btc.Address, + feeRate?: number, ) { - const feeRate = await this.getFeeRate(); + // const feeRate = await this.getFeeRate(); let feeUtxos = await getUtxos( this.configService, @@ -137,8 +138,9 @@ export class SendCommand extends BoardcastCommand { }); if (feeUtxos.length === 0) { - console.warn('Insufficient satoshis balance!'); - return; + console.info("Insufficient satoshis balance!"); + throw new Error("Insufficient satoshis balance!"); + // return; } const res = await getTokens( @@ -157,7 +159,7 @@ export class SendCommand extends BoardcastCommand { let tokenContracts = pick(contracts, amount); if (tokenContracts.length === 0) { - console.warn('Insufficient token balance!'); + console.warn("Insufficient token balance!"); return; } @@ -177,7 +179,7 @@ export class SendCommand extends BoardcastCommand { ); if (e instanceof Error) { - logerror('merge token failed!', e); + logerror("merge token failed!", e); return; } @@ -232,8 +234,8 @@ export class SendCommand extends BoardcastCommand { } @Option({ - flags: '-i, --id [tokenId]', - description: 'ID of the token', + flags: "-i, --id [tokenId]", + description: "ID of the token", }) parseId(val: string): string { return val; diff --git a/packages/cli/src/commands/wallet/address.command.ts b/packages/cli/src/commands/wallet/address.command.ts index 742d27bf..3fe94bee 100644 --- a/packages/cli/src/commands/wallet/address.command.ts +++ b/packages/cli/src/commands/wallet/address.command.ts @@ -1,14 +1,14 @@ -import { SubCommand } from 'nest-commander'; -import { log, logerror } from 'src/common'; -import { BaseCommand, BaseCommandOptions } from '../base.command'; -import { ConfigService, WalletService } from 'src/providers'; -import { Inject } from '@nestjs/common'; +import { SubCommand } from "nest-commander"; +import { log, logerror } from "../../common"; +import { BaseCommand, BaseCommandOptions } from "../base.command"; +import { ConfigService, WalletService } from "../../providers"; +import { Inject } from "@nestjs/common"; interface AddressCommandOptions extends BaseCommandOptions {} @SubCommand({ - name: 'address', - description: 'Show address', + name: "address", + description: "Show address", }) export class AddressCommand extends BaseCommand { constructor( @@ -29,7 +29,7 @@ export class AddressCommand extends BaseCommand { log(`Your address is ${address}`); } catch (error) { - logerror('Get address failed!', error); + logerror("Get address failed!", error); } } } diff --git a/packages/cli/src/commands/wallet/balance.command.ts b/packages/cli/src/commands/wallet/balance.command.ts index b7be3f5f..6c3c35b4 100644 --- a/packages/cli/src/commands/wallet/balance.command.ts +++ b/packages/cli/src/commands/wallet/balance.command.ts @@ -1,25 +1,25 @@ -import { Option, SubCommand } from 'nest-commander'; +import { Option, SubCommand } from "nest-commander"; import { getBalance, getAllBalance, logerror, unScaleByDecimals, getTrackerStatus, -} from 'src/common'; -import { BaseCommand, BaseCommandOptions } from '../base.command'; -import { ConfigService, WalletService } from 'src/providers'; -import { Inject } from '@nestjs/common'; -import { findTokenMetadataById } from 'src/token'; -import { table } from './table'; -import Decimal from 'decimal.js'; +} from "../../common"; +import { BaseCommand, BaseCommandOptions } from "../base.command"; +import { ConfigService, WalletService } from "../../providers"; +import { Inject } from "@nestjs/common"; +import { findTokenMetadataById } from "../../token"; +import { table } from "./table"; +import Decimal from "decimal.js"; interface BalanceCommandOptions extends BaseCommandOptions { id?: string; } @SubCommand({ - name: 'balances', - description: 'Get balances of all tokens', + name: "balances", + description: "Get balances of all tokens", }) export class BalanceCommand extends BaseCommand { constructor( @@ -32,13 +32,13 @@ export class BalanceCommand extends BaseCommand { async checkTrackerStatus() { const status = await getTrackerStatus(this.configService); if (status instanceof Error) { - throw new Error('tracker status is abnormal'); + throw new Error("tracker status is abnormal"); } const { trackerBlockHeight, latestBlockHeight } = status; if (trackerBlockHeight < latestBlockHeight) { - console.warn('tracker is behind latest blockchain height'); + console.warn("tracker is behind latest blockchain height"); console.warn( `processing ${trackerBlockHeight}/${latestBlockHeight}: ${new Decimal(trackerBlockHeight).div(latestBlockHeight).mul(100).toFixed(0)}%`, ); @@ -81,7 +81,7 @@ export class BalanceCommand extends BaseCommand { const balances = await getAllBalance(this.configService, address); if (balances.length === 0) { - console.log('No tokens found!'); + console.log("No tokens found!"); await this.checkTrackerStatus(); return; } @@ -107,13 +107,13 @@ export class BalanceCommand extends BaseCommand { console.log(table(all)); } } catch (error) { - logerror('Get Balance failed!', error); + logerror("Get Balance failed!", error); } } @Option({ - flags: '-i, --id [tokenId]', - description: 'ID of the token', + flags: "-i, --id [tokenId]", + description: "ID of the token", }) parseId(val: string): string { return val; diff --git a/packages/cli/src/commands/wallet/create.command.ts b/packages/cli/src/commands/wallet/create.command.ts index 22fdd3db..5f7254d9 100644 --- a/packages/cli/src/commands/wallet/create.command.ts +++ b/packages/cli/src/commands/wallet/create.command.ts @@ -1,18 +1,18 @@ -import { Option, InquirerService, SubCommand } from 'nest-commander'; -import { BaseCommand, BaseCommandOptions } from '../base.command'; -import { logerror, Wallet } from 'src/common'; -import { ConfigService, WalletService } from 'src/providers'; -import { Inject } from '@nestjs/common'; -import { randomBytes } from 'crypto'; -import * as bip39 from 'bip39'; +import { Option, InquirerService, SubCommand } from "nest-commander"; +import { BaseCommand, BaseCommandOptions } from "../base.command"; +import { logerror, Wallet } from "../../common"; +import { ConfigService, WalletService } from "../../providers"; +import { Inject } from "@nestjs/common"; +import { randomBytes } from "crypto"; +import * as bip39 from "bip39"; interface CreateCommandOptions extends BaseCommandOptions { name: string; } @SubCommand({ - name: 'create', - description: 'Create a wallet.', + name: "create", + description: "Create a wallet.", }) export class CreateCommand extends BaseCommand { constructor( @@ -37,36 +37,40 @@ export class CreateCommand extends BaseCommand { const name = options.name ? options.name - : `cat-${randomBytes(4).toString('hex')}`; + : `cat-${randomBytes(4).toString("hex")}`; + const privateKey = inputs[0]; const wallet: Wallet = { accountPath: "m/86'/0'/0'/0/0", name: name, mnemonic: bip39.generateMnemonic(), + privateKey: privateKey, + address: "", + pubKey: null, }; this.walletService.createWallet(wallet); - console.log('Your wallet mnemonic is: ', wallet.mnemonic); + console.log("Your wallet mnemonic is: ", wallet.mnemonic); - console.log('exporting address to the RPC node ... '); + console.log("exporting address to the RPC node ... "); const success = await this.walletService.importWallet(true); if (success) { - console.log('successfully.'); + console.log("successfully."); } } catch (error) { - logerror('Create wallet failed!', error); + logerror("Create wallet failed!", error); } } @Option({ - flags: '-n,--name [name]', - description: 'wallet name', + flags: "-n,--name [name]", + description: "wallet name", }) parseName(val: string): string { if (!val) { - logerror("wallet name can't be empty!", new Error('invalid name option')); + logerror("wallet name can't be empty!", new Error("invalid name option")); process.exit(0); } diff --git a/packages/cli/src/commands/wallet/import.command.ts b/packages/cli/src/commands/wallet/import.command.ts index a84dc7e9..d020fbc9 100644 --- a/packages/cli/src/commands/wallet/import.command.ts +++ b/packages/cli/src/commands/wallet/import.command.ts @@ -1,16 +1,16 @@ -import { SubCommand, Option } from 'nest-commander'; -import { BaseCommand, BaseCommandOptions } from '../base.command'; -import { log, logerror } from 'src/common'; -import { ConfigService, WalletService } from 'src/providers'; -import { Inject } from '@nestjs/common'; +import { SubCommand, Option } from "nest-commander"; +import { BaseCommand, BaseCommandOptions } from "../base.command"; +import { log, logerror } from "../../common"; +import { ConfigService, WalletService } from "../../providers"; +import { Inject } from "@nestjs/common"; interface ImportCommandOptions extends BaseCommandOptions { create: boolean; } @SubCommand({ - name: 'export', - description: 'Export wallet to a RPC node.', + name: "export", + description: "Export wallet to a RPC node.", }) export class ImportCommand extends BaseCommand { constructor( @@ -27,25 +27,25 @@ export class ImportCommand extends BaseCommand { ): Promise { try { if (!this.configService.useRpc()) { - log('Please config your rpc first!'); + log("Please config your rpc first!"); return; } - console.log('exporting address to the RPC node ... '); + console.log("exporting address to the RPC node ... "); const success = await this.walletService.importWallet(options.create); if (success) { - console.log('successfully.'); + console.log("successfully."); } } catch (error) { - logerror('exporting address to the RPC node failed!', error); + logerror("exporting address to the RPC node failed!", error); } } @Option({ - flags: '--create [create]', + flags: "--create [create]", defaultValue: false, - description: 'create watch only wallet before import address', + description: "create watch only wallet before import address", }) // eslint-disable-next-line @typescript-eslint/no-unused-vars parseCreate(val: string): boolean { diff --git a/packages/cli/src/commands/wallet/wallet.command.ts b/packages/cli/src/commands/wallet/wallet.command.ts index 4409df4b..91d1f8fc 100644 --- a/packages/cli/src/commands/wallet/wallet.command.ts +++ b/packages/cli/src/commands/wallet/wallet.command.ts @@ -3,7 +3,7 @@ import { AddressCommand } from './address.command'; import { CreateCommand } from './create.command'; import { BalanceCommand } from './balance.command'; import { ImportCommand } from './import.command'; -import { logerror } from 'src/common'; +import { logerror } from '../../common'; interface WalletCommandOptions {} @Command({ diff --git a/packages/cli/src/common/apis-rpc.ts b/packages/cli/src/common/apis-rpc.ts index dc71a678..19c49003 100644 --- a/packages/cli/src/common/apis-rpc.ts +++ b/packages/cli/src/common/apis-rpc.ts @@ -1,9 +1,9 @@ -import { UTXO } from 'scrypt-ts'; -import { Decimal } from 'decimal.js'; -import * as descriptors from '@bitcoinerlab/descriptors'; -import { logerror } from './log'; -import { ConfigService } from 'src/providers'; -import fetch from 'node-fetch-cjs'; +import { UTXO } from "scrypt-ts"; +import { Decimal } from "decimal.js"; +import * as descriptors from "@bitcoinerlab/descriptors"; +import { logerror } from "./log"; +import { ConfigService } from "../providers"; +import fetch from "node-fetch-cjs"; /** * only for localhost @@ -17,24 +17,24 @@ export const rpc_broadcast = async function ( ): Promise { const Authorization = `Basic ${Buffer.from( `${config.getRpcUser()}:${config.getRpcPassword()}`, - ).toString('base64')}`; + ).toString("base64")}`; return fetch(config.getRpcUrl(walletName), { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", Authorization, }, body: JSON.stringify({ - jsonrpc: '2.0', - id: 'cat-cli', - method: 'sendrawtransaction', + jsonrpc: "2.0", + id: "cat-cli", + method: "sendrawtransaction", params: [txHex], }), }) .then((res) => { - const contentType = res.headers.get('content-type'); - if (contentType.includes('json')) { + const contentType = res.headers.get("content-type"); + if (contentType.includes("json")) { return res.json(); } else { throw new Error( @@ -60,24 +60,24 @@ export const rpc_getrawtransaction = async function ( ): Promise { const Authorization = `Basic ${Buffer.from( `${config.getRpcUser()}:${config.getRpcPassword()}`, - ).toString('base64')}`; + ).toString("base64")}`; return fetch(config.getRpcUrl(walletName), { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", Authorization, }, body: JSON.stringify({ - jsonrpc: '2.0', - id: 'cat-cli', - method: 'getrawtransaction', + jsonrpc: "2.0", + id: "cat-cli", + method: "getrawtransaction", params: [txid], }), }) .then((res) => { - const contentType = res.headers.get('content-type'); - if (contentType.includes('json')) { + const contentType = res.headers.get("content-type"); + if (contentType.includes("json")) { return res.json(); } else { throw new Error( @@ -92,7 +92,7 @@ export const rpc_getrawtransaction = async function ( return res.result; }) .catch((e) => { - logerror('broadcast_rpc failed!', e); + logerror("broadcast_rpc failed!", e); return e; }); }; @@ -109,24 +109,24 @@ export const rpc_getconfirmations = async function ( > { const Authorization = `Basic ${Buffer.from( `${config.getRpcUser()}:${config.getRpcPassword()}`, - ).toString('base64')}`; + ).toString("base64")}`; return fetch(config.getRpcUrl(null), { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", Authorization, }, body: JSON.stringify({ - jsonrpc: '2.0', - id: 'cat-cli', - method: 'getrawtransaction', + jsonrpc: "2.0", + id: "cat-cli", + method: "getrawtransaction", params: [txid, true], }), }) .then((res) => { - const contentType = res.headers.get('content-type'); - if (contentType.includes('json')) { + const contentType = res.headers.get("content-type"); + if (contentType.includes("json")) { return res.json(); } else { throw new Error( @@ -140,7 +140,7 @@ export const rpc_getconfirmations = async function ( } return { confirmations: -1, - blockhash: '', + blockhash: "", ...res.result, }; }) @@ -155,24 +155,24 @@ export const rpc_getfeeRate = async function ( ): Promise { const Authorization = `Basic ${Buffer.from( `${config.getRpcUser()}:${config.getRpcPassword()}`, - ).toString('base64')}`; + ).toString("base64")}`; return fetch(config.getRpcUrl(walletName), { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", Authorization, }, body: JSON.stringify({ - jsonrpc: '2.0', - id: 'cat-cli', - method: 'estimatesmartfee', + jsonrpc: "2.0", + id: "cat-cli", + method: "estimatesmartfee", params: [1], }), }) .then((res) => { - const contentType = res.headers.get('content-type'); - if (contentType.includes('json')) { + const contentType = res.headers.get("content-type"); + if (contentType.includes("json")) { return res.json(); } else { throw new Error( @@ -205,23 +205,25 @@ export const rpc_listunspent = async function ( ): Promise { const Authorization = `Basic ${Buffer.from( `${config.getRpcUser()}:${config.getRpcPassword()}`, - ).toString('base64')}`; + ).toString("base64")}`; return fetch(config.getRpcUrl(walletName), { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", Authorization, }, body: JSON.stringify({ - jsonrpc: '2.0', - id: 'cat-cli', - method: 'listunspent', + jsonrpc: "2.0", + id: "cat-cli", + method: "listunspent", params: [0, 9999999, [address]], }), }) .then((res) => { if (res.status === 200) { + console.log("---->res* ", res); + return res.json(); } throw new Error(res.statusText); @@ -231,7 +233,12 @@ export const rpc_listunspent = async function ( throw new Error(JSON.stringify(res)); } + console.log("---->res ", res); + + console.log("---->res.result ", res.result); + const utxos: UTXO[] = res.result.map((item: any) => { + console.log("---->item ", item); return { txId: item.txid, outputIndex: item.vout, @@ -245,6 +252,8 @@ export const rpc_listunspent = async function ( return utxos; }) .catch((e: Error) => { + console.log("---->err ", e); + return e; }); }; @@ -255,23 +264,23 @@ export const rpc_create_watchonly_wallet = async function ( ): Promise { const Authorization = `Basic ${Buffer.from( `${config.getRpcUser()}:${config.getRpcPassword()}`, - ).toString('base64')}`; + ).toString("base64")}`; return fetch(config.getRpcUrl(null), { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", Authorization, }, body: JSON.stringify({ - jsonrpc: '2.0', - id: 'cat-cli', - method: 'createwallet', + jsonrpc: "2.0", + id: "cat-cli", + method: "createwallet", params: { wallet_name: walletName, disable_private_keys: true, blank: true, - passphrase: '', + passphrase: "", descriptors: true, load_on_startup: true, }, @@ -301,21 +310,21 @@ export const rpc_importdescriptors = async function ( ): Promise { const Authorization = `Basic ${Buffer.from( `${config.getRpcUser()}:${config.getRpcPassword()}`, - ).toString('base64')}`; + ).toString("base64")}`; const checksum = descriptors.checksum(desc); const timestamp = Math.ceil(new Date().getTime() / 1000); return fetch(config.getRpcUrl(walletName), { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", Authorization, }, body: JSON.stringify({ - jsonrpc: '2.0', - id: 'cat-cli', - method: 'importdescriptors', + jsonrpc: "2.0", + id: "cat-cli", + method: "importdescriptors", params: [ [ { @@ -324,7 +333,7 @@ export const rpc_importdescriptors = async function ( index: 0, internal: false, timestamp, - label: '', + label: "", }, ], ], diff --git a/packages/cli/src/common/apis-tracker.ts b/packages/cli/src/common/apis-tracker.ts index 9d4fbb98..cfb9f100 100644 --- a/packages/cli/src/common/apis-tracker.ts +++ b/packages/cli/src/common/apis-tracker.ts @@ -2,11 +2,11 @@ import { OpenMinterState, ProtocolState, ProtocolStateList, -} from '@cat-protocol/cat-smartcontracts'; -import { OpenMinterContract, TokenContract } from './contact'; -import { OpenMinterTokenInfo, TokenMetadata } from './metadata'; -import { isOpenMinter } from './minterFinder'; -import { getRawTransaction } from './apis'; +} from "@cat-protocol/cat-smartcontracts"; +import { OpenMinterContract, TokenContract } from "./contact"; +import { OpenMinterTokenInfo, TokenMetadata } from "./metadata"; +import { isOpenMinter } from "./minterFinder"; +import { getRawTransaction } from "./apis"; import { getTokenContractP2TR, p2tr2Address, @@ -212,7 +212,7 @@ export const getTokenMinter = async function ( ); } - if (typeof c.utxo.satoshis === 'string') { + if (typeof c.utxo.satoshis === "string") { c.utxo.satoshis = parseInt(c.utxo.satoshis); } @@ -226,7 +226,7 @@ export const getTokenMinter = async function ( }), ); } else { - throw new Error('Unkown minter!'); + throw new Error("Unkown minter!"); } }) .then((minters) => { @@ -247,58 +247,80 @@ export const getTokens = async function ( trackerBlockHeight: number; contracts: Array; } | null> { - const url = `${config.getTracker()}/api/tokens/${metadata.tokenId}/addresses/${ownerAddress}/utxos`; - return fetch(url, config.withProxy()) - .then((res) => res.json()) - .then((res: any) => { - if (res.code === 0) { - return res.data; - } else { - throw new Error(res.msg); - } - }) - .then(({ utxos, trackerBlockHeight }) => { - let contracts: Array = utxos.map((c) => { - const protocolState = ProtocolState.fromStateHashList( - c.txoStateHashes as ProtocolStateList, - ); - if (typeof c.utxo.satoshis === 'string') { - c.utxo.satoshis = parseInt(c.utxo.satoshis); + let offset = 0; + let allContracts: Array = []; + let finalTrackerBlockHeight; + let isFinished = false; + + while (true) { + const url = `${config.getTracker()}/api/tokens/${metadata.tokenId}/addresses/${ownerAddress}/utxos?limit=100&offset=${offset}`; + await fetch(url, config.withProxy()) + .then((res) => res.json()) + .then((res: any) => { + if (res.code === 0) { + return res.data; + } else { + throw new Error(res.msg); } + }) + .then(({ utxos, trackerBlockHeight }) => { + let contracts: Array = utxos.map((c) => { + const protocolState = ProtocolState.fromStateHashList( + c.txoStateHashes as ProtocolStateList, + ); - const r: TokenContract = { - utxo: c.utxo, - state: { - protocolState, - data: { - ownerAddr: c.state.address, - amount: BigInt(c.state.amount), + if (typeof c.utxo.satoshis === "string") { + c.utxo.satoshis = parseInt(c.utxo.satoshis); + } + + const r: TokenContract = { + utxo: c.utxo, + state: { + protocolState, + data: { + ownerAddr: c.state.address, + amount: BigInt(c.state.amount), + }, }, - }, - }; + }; - return r; - }); + return r; + }); - contracts = contracts.filter((tokenContract) => { - return spendService.isUnspent(tokenContract.utxo); - }); + contracts = contracts.filter((tokenContract) => { + return spendService.isUnspent(tokenContract.utxo); + }); - if (trackerBlockHeight - spendService.blockHeight() > 100) { - spendService.reset(); - } - spendService.updateBlockHeight(trackerBlockHeight); + allContracts.push(...contracts); + finalTrackerBlockHeight = trackerBlockHeight; + if (contracts.length == 0) { + isFinished = true; + } + offset += 100; - return { - contracts, - trackerBlockHeight: trackerBlockHeight as number, - }; - }) - .catch((e) => { - logerror(`fetch tokens failed:`, e); - return null; - }); + if (trackerBlockHeight - spendService.blockHeight() > 100) { + spendService.reset(); + } + spendService.updateBlockHeight(trackerBlockHeight); + + // return { + // contracts, + // trackerBlockHeight: trackerBlockHeight as number, + // }; + }) + .catch((e) => { + logerror(`fetch tokens failed:`, e); + return null; + }); + if (isFinished) { + break; + } + } + + return { + contracts: allContracts, trackerBlockHeight: finalTrackerBlockHeight, + } }; export const getAllBalance = async function ( @@ -377,10 +399,10 @@ export const getBalance = async function ( export const getTrackerStatus = async function (config: ConfigService): Promise< | { - trackerBlockHeight: number; - nodeBlockHeight: number; - latestBlockHeight: number; - } + trackerBlockHeight: number; + nodeBlockHeight: number; + latestBlockHeight: number; + } | Error > { const url = `${config.getTracker()}/api`; diff --git a/packages/cli/src/common/apis.ts b/packages/cli/src/common/apis.ts index 0ee6bbc5..543dd0e9 100644 --- a/packages/cli/src/common/apis.ts +++ b/packages/cli/src/common/apis.ts @@ -1,5 +1,5 @@ -import { UTXO } from 'scrypt-ts'; -import fetch from 'node-fetch-cjs'; +import { UTXO } from "scrypt-ts"; +import fetch from "node-fetch-cjs"; import { rpc_broadcast, @@ -7,10 +7,10 @@ import { rpc_getfeeRate, rpc_getrawtransaction, rpc_listunspent, -} from './apis-rpc'; -import { logerror, logwarn } from './log'; -import { btc } from './btc'; -import { ConfigService, WalletService } from 'src/providers'; +} from "./apis-rpc"; +import { logerror, logwarn } from "./log"; +import { btc } from "./btc"; +import { ConfigService, WalletService } from "../providers"; export const getFeeRate = async function ( config: ConfigService, @@ -41,7 +41,7 @@ export const getFeeRate = async function ( return 2; } - return Math.max(2, feeRate['fastestFee'] || 1); + return Math.max(2, feeRate["fastestFee"] || 1); }; export const getFractalUtxos = async function ( @@ -54,16 +54,16 @@ export const getFractalUtxos = async function ( const utxos: Array = await fetch( url, config.withProxy({ - method: 'GET', + method: "GET", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", Authorization: `Bearer ${config.getApiKey()}`, }, }), ) .then(async (res) => { - const contentType = res.headers.get('content-type'); - if (contentType.includes('json')) { + const contentType = res.headers.get("content-type"); + if (contentType.includes("json")) { return res.json(); } else { throw new Error( @@ -72,6 +72,7 @@ export const getFractalUtxos = async function ( } }) .then((res: any) => { + console.log("---->res", res); if (res.code === 0) { const { data } = res; return data.utxo.map((utxo) => { @@ -106,6 +107,9 @@ export const getUtxos = async function ( wallet.getWalletName(), address.toString(), ); + console.log("---->config.useRpc() "); + console.log("---->wallet.getWalletName() ", wallet.getWalletName()); + console.log("---->address.toString() ", address.toString()); if (utxos instanceof Error) { return []; } @@ -113,6 +117,8 @@ export const getUtxos = async function ( } if (config.isFractalNetwork() && !config.useRpc()) { + console.log("---->config.isFractalNetwork()"); + return getFractalUtxos(config, address); } @@ -121,8 +127,8 @@ export const getUtxos = async function ( const url = `${config.getMempoolApiHost()}/api/address/${address}/utxo`; const utxos: Array = await fetch(url, config.withProxy()) .then(async (res) => { - const contentType = res.headers.get('content-type'); - if (contentType.includes('json')) { + const contentType = res.headers.get("content-type"); + if (contentType.includes("json")) { return res.json(); } else { throw new Error( @@ -170,7 +176,7 @@ export const getRawTransaction = async function ( }) // eslint-disable-next-line @typescript-eslint/no-unused-vars .catch((e: Error) => { - logerror('getrawtransaction failed!', e); + logerror("getrawtransaction failed!", e); return e; }) ); @@ -190,9 +196,9 @@ export const getConfirmations = async function ( return rpc_getconfirmations(config, txid); } - logwarn('No supported getconfirmations', new Error()); + logwarn("No supported getconfirmations", new Error()); return { - blockhash: '', + blockhash: "", confirmations: -1, }; }; @@ -210,29 +216,29 @@ export async function broadcast( return fetch( url, config.withProxy({ - method: 'POST', + method: "POST", body: txHex, }), ) .then(async (res) => { - const contentType = res.headers.get('content-type'); - if (contentType.includes('json')) { + const contentType = res.headers.get("content-type"); + if (contentType.includes("json")) { return res.json(); } else { return res.text(); } }) .then(async (data) => { - if (typeof data === 'string' && data.length === 64) { + if (typeof data === "string" && data.length === 64) { return data; - } else if (typeof data === 'object') { + } else if (typeof data === "object") { throw new Error(JSON.stringify(data)); - } else if (typeof data === 'string') { + } else if (typeof data === "string") { throw new Error(data); } }) .catch((e) => { - logerror('broadcast failed!', e); + logerror("broadcast failed!", e); return e; }); } diff --git a/packages/cli/src/common/wallet.ts b/packages/cli/src/common/wallet.ts index 751f5cb9..ec305a03 100644 --- a/packages/cli/src/common/wallet.ts +++ b/packages/cli/src/common/wallet.ts @@ -1,6 +1,6 @@ export enum AddressType { - P2WPKH = 'p2wpkh', - P2TR = 'p2tr', + P2WPKH = "p2wpkh", + P2TR = "p2tr", } export interface Wallet { @@ -8,4 +8,7 @@ export interface Wallet { accountPath: string; addressType?: AddressType; mnemonic: string; + privateKey: string; + address: string; + pubKey: Buffer; } diff --git a/packages/cli/src/providers/configService.ts b/packages/cli/src/providers/configService.ts index cc692345..1359ded0 100644 --- a/packages/cli/src/providers/configService.ts +++ b/packages/cli/src/providers/configService.ts @@ -1,16 +1,16 @@ -import { Injectable } from '@nestjs/common'; -import { accessSync, constants, readFileSync } from 'fs'; -import { CliConfig } from 'src/common'; -import { isAbsolute, join } from 'path'; -import { HttpsProxyAgent } from 'https-proxy-agent'; +import { Injectable } from "@nestjs/common"; +import { accessSync, constants, readFileSync } from "fs"; +import { CliConfig } from "../common"; +import { isAbsolute, join } from "path"; +import { HttpsProxyAgent } from "https-proxy-agent"; @Injectable() export class ConfigService { constructor() {} cliConfig: CliConfig = { - network: 'fractal-mainnet', - tracker: 'http://127.0.0.1:3000', - dataDir: '.', + network: "fractal-mainnet", + tracker: "http://127.0.0.1:3000", + dataDir: ".", feeRate: -1, maxFeeRate: -1, rpc: null, @@ -22,7 +22,7 @@ export class ConfigService { Object.assign(rpc, this.cliConfig.rpc); } - if (one.rpc !== null && typeof one.rpc === 'object') { + if (one.rpc !== null && typeof one.rpc === "object") { Object.assign(rpc, one.rpc); } @@ -43,7 +43,7 @@ export class ConfigService { } try { - const config = JSON.parse(readFileSync(configPath, 'utf8')); + const config = JSON.parse(readFileSync(configPath, "utf8")); this.mergeCliConfig(config); return null; } catch (error) { @@ -68,12 +68,12 @@ export class ConfigService { getMempoolApiHost = () => { const config = this.getCliConfig(); - if (config.network === 'btc-signet') { - return 'https://mempool.space/signet'; - } else if (config.network === 'fractal-testnet') { - return 'https://mempool-testnet.fractalbitcoin.io'; - } else if (config.network === 'fractal-mainnet') { - return 'https://mempool.fractalbitcoin.io'; + if (config.network === "btc-signet") { + return "https://mempool.space/signet"; + } else if (config.network === "fractal-testnet") { + return "https://mempool-testnet.fractalbitcoin.io"; + } else if (config.network === "fractal-mainnet") { + return "https://mempool.fractalbitcoin.io"; } else { throw new Error(`Unsupport network: ${config.network}`); } @@ -158,7 +158,7 @@ export class ConfigService { isFractalNetwork = () => { const config = this.getCliConfig(); - return config.network.startsWith('fractal'); + return config.network.startsWith("fractal"); }; getDataDir(): string { diff --git a/packages/cli/src/providers/spendService.ts b/packages/cli/src/providers/spendService.ts index 28e73d5c..eea6075d 100644 --- a/packages/cli/src/providers/spendService.ts +++ b/packages/cli/src/providers/spendService.ts @@ -1,9 +1,9 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { UTXO } from 'scrypt-ts'; -import { ConfigService } from './configService'; -import { join } from 'path'; -import { existsSync, readFileSync, writeFileSync } from 'fs'; -import { logerror, btc } from 'src/common'; +import { Inject, Injectable } from "@nestjs/common"; +import { UTXO } from "scrypt-ts"; +import { ConfigService } from "./configService"; +import { join } from "path"; +import { existsSync, readFileSync, writeFileSync } from "fs"; +import { logerror, btc } from "../common"; @Injectable() export class SpendService { @@ -15,7 +15,7 @@ export class SpendService { loadSpends() { const dataDir = this.configService.getDataDir(); - const spendFile = join(dataDir, 'spends.json'); + const spendFile = join(dataDir, "spends.json"); let spendString = null; try { @@ -43,7 +43,7 @@ export class SpendService { } addSpend(spend: UTXO | string) { - if (typeof spend === 'string') { + if (typeof spend === "string") { this.spends.add(spend); } else { const utxo = spend as UTXO; @@ -52,7 +52,7 @@ export class SpendService { } isUnspent(utxo: UTXO | string): boolean { - if (typeof utxo === 'string') { + if (typeof utxo === "string") { return !this.spends.has(utxo); } return !this.spends.has(`${utxo.txId}:${utxo.outputIndex}`); @@ -61,7 +61,7 @@ export class SpendService { updateSpends(tx: btc.Transaction) { for (let i = 0; i < tx.inputs.length - 1; i++) { const input = tx.inputs[i]; - this.addSpend(`${input.prevTxId.toString('hex')}:${input.outputIndex}`); + this.addSpend(`${input.prevTxId.toString("hex")}:${input.outputIndex}`); } } @@ -85,7 +85,7 @@ export class SpendService { save(): void { const dataDir = this.configService.getDataDir(); - const spendsFile = join(dataDir, 'spends.json'); + const spendsFile = join(dataDir, "spends.json"); try { writeFileSync( spendsFile, diff --git a/packages/cli/src/providers/walletService.ts b/packages/cli/src/providers/walletService.ts index 1b0ff6f6..42ea7941 100644 --- a/packages/cli/src/providers/walletService.ts +++ b/packages/cli/src/providers/walletService.ts @@ -1,11 +1,11 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore -import btc = require('bitcore-lib-inquisition'); -import * as bip39 from 'bip39'; -import BIP32Factory from 'bip32'; -import * as ecc from 'tiny-secp256k1'; -import { Inject, Injectable } from '@nestjs/common'; -import { existsSync, readFileSync, writeFileSync } from 'fs'; +import btc = require("bitcore-lib-inquisition"); +import * as bip39 from "bip39"; +import BIP32Factory from "bip32"; +import * as ecc from "tiny-secp256k1"; +import { Inject, Injectable } from "@nestjs/common"; +import { existsSync, readFileSync, writeFileSync } from "fs"; import { AddressType, logerror, @@ -13,51 +13,55 @@ import { rpc_importdescriptors, toXOnly, Wallet, -} from 'src/common'; -import { ConfigService } from './configService'; -import { join } from 'path'; -import { hash160 } from 'scrypt-ts'; +} from "../common"; +import { ConfigService } from "./configService"; +import { join } from "path"; +import { hash160 } from "scrypt-ts"; +import { network } from "../../../tracker/src/common/constants"; +import { randomBytes } from "crypto"; const bip32 = BIP32Factory(ecc); +const BitcoinNetwork = btc.Networks.mainnet; + @Injectable() export class WalletService { private wallet: Wallet | null = null; - constructor(@Inject() private readonly configService: ConfigService) {} + constructor(@Inject() private readonly configService: ConfigService) { } checkWalletJson(obj: any) { - if (typeof obj.name === 'undefined') { + if (typeof obj.name === "undefined") { throw new Error('No "name" found in wallet.json!'); } - if (typeof obj.name !== 'string') { + if (typeof obj.name !== "string") { throw new Error('"name" in wallet.json should be string!'); } - if (typeof obj.mnemonic === 'undefined') { + if (typeof obj.mnemonic === "undefined") { throw new Error('No "mnemonic" found in wallet.json!'); } - if (typeof obj.mnemonic !== 'string') { + if (typeof obj.mnemonic !== "string") { throw new Error('"mnemonic" in wallet.json should be string!'); } if (!bip39.validateMnemonic(obj.mnemonic)) { - throw new Error('Invalid mnemonic in wallet.json!'); + throw new Error("Invalid mnemonic in wallet.json!"); } - if (typeof obj.accountPath === 'undefined') { + if (typeof obj.accountPath === "undefined") { throw new Error('No "accountPath" found in wallet.json!'); } - if (typeof obj.accountPath !== 'string') { + if (typeof obj.accountPath !== "string") { throw new Error('"accountPath" in wallet.json should be string!'); } } loadWallet(): Wallet | null { const dataDir = this.configService.getDataDir(); - const walletFile = join(dataDir, 'wallet.json'); + const walletFile = join(dataDir, "wallet.json"); let walletString = null; try { @@ -107,7 +111,7 @@ export class WalletService { if (wallet === null) { throw new Error("run 'create wallet' command to create a wallet."); } - return wallet.accountPath || ''; + return wallet.accountPath || ""; }; getMnemonic = () => { @@ -122,36 +126,72 @@ export class WalletService { return this.getPrivateKey().toWIF(); } - getPrivateKey(derivePath?: string): btc.PrivateKey { - const mnemonic = this.getMnemonic(); + getPrivateKey(): btc.PrivateKey { const network = btc.Networks.mainnet; - return derivePrivateKey( - mnemonic, - derivePath || this.getAccountPath(), - network, - ); + return new btc.PrivateKey(this.wallet.privateKey, network); + } + + overwriteWallet(privateKey: string) { + //TODO: kelvin validate privateKeys + const name = `cat-${randomBytes(4).toString("hex")}`; + + const wallet: Wallet = { + accountPath: "m/86'/0'/0'/0/0", + name: null, + mnemonic: bip39.generateMnemonic(), + privateKey: privateKey, + address: "", + pubKey: null, + }; + + this.createWallet(wallet); + } + + + overwriteWalletByAddress(address: string, pubKey: Buffer) { + // use fake mnem & priv key + const mnem = bip39.generateMnemonic(); + const accPath = "m/86'/0'/0'/0/0"; + const privKey = derivePrivateKey(mnem, accPath, BitcoinNetwork); // hard code mainnet + const wallet: Wallet = { + addressType: AddressType.P2TR, + accountPath: accPath, + name: null, + mnemonic: mnem, + privateKey: privKey, + address: address, + pubKey: pubKey, + }; + + this.createWallet(wallet); } /** * Generate a derive path from the given seed */ generateDerivePath(seed: string): string { - const path = ['m']; - const hash = Buffer.from(hash160(seed), 'hex'); + const path = ["m"]; + const hash = Buffer.from(hash160(seed), "hex"); for (let i = 0; i < hash.length; i += 4) { let index = hash.readUint32BE(i); - let hardened = ''; + let hardened = ""; if (index >= 0x80000000) { index -= 0x80000000; hardened = "'"; } path.push(`${index}${hardened}`); } - return path.join('/'); + return path.join("/"); } getP2TRAddress(): btc.Address { - return this.getPrivateKey().toAddress(null, btc.Address.PayToTaproot); + if (this.wallet.address === "") { + return this.getPrivateKey().toAddress(null, btc.Address.PayToTaproot); + } + + const address = btc.Address.fromString(this.wallet.address, BitcoinNetwork, 'pubkey'); + return address; + } getAddress(): btc.Address { @@ -160,7 +200,7 @@ export class WalletService { getXOnlyPublicKey(): string { const pubkey = this.getPublicKey(); - return toXOnly(pubkey.toBuffer()).toString('hex'); + return toXOnly(pubkey.toBuffer()).toString("hex"); } getTweakedPrivateKey(): btc.PrivateKey { @@ -169,6 +209,10 @@ export class WalletService { } getPublicKey(): btc.PublicKey { + if (this.wallet.pubKey !== null) { + return btc.PublicKey.fromBuffer(this.wallet.pubKey); + } + const addressType = this.getAddressType(); if (addressType === AddressType.P2TR) { @@ -181,7 +225,7 @@ export class WalletService { getPubKeyPrefix(): string { const addressType = this.getAddressType(); if (addressType === AddressType.P2TR) { - return ''; + return ""; } else if (addressType === AddressType.P2WPKH) { const pubkey = this.getPublicKey(); return pubkey.toString().slice(0, 2); @@ -220,7 +264,7 @@ export class WalletService { createWallet(wallet: Wallet): Error | null { const dataDir = this.configService.getDataDir(); - const walletFile = join(dataDir, 'wallet.json'); + const walletFile = join(dataDir, "wallet.json"); try { writeFileSync(walletFile, JSON.stringify(wallet, null, 2)); this.wallet = wallet; @@ -238,7 +282,7 @@ export class WalletService { this.wallet.name, ); if (e instanceof Error) { - logerror('rpc_create_watchonly_wallet failed!', e); + logerror("rpc_create_watchonly_wallet failed!", e); return false; } } @@ -249,7 +293,7 @@ export class WalletService { ); if (importError instanceof Error) { - logerror('rpc_importdescriptors failed!', importError); + logerror("rpc_importdescriptors failed!", importError); return false; } @@ -258,25 +302,29 @@ export class WalletService { foundWallet(): string | null { const dataDir = this.configService.getDataDir(); - const walletFile = join(dataDir, 'wallet.json'); + const walletFile = join(dataDir, "wallet.json"); let walletString = null; try { walletString = readFileSync(walletFile).toString(); JSON.parse(walletString); return walletFile; - } catch (error) {} + } catch (error) { } return null; } signTx(tx: btc.Transaction) { + if (this.wallet.address !== "") { + return + } // unlock fee inputs const privateKey = this.getPrivateKey(); const hashData = btc.crypto.Hash.sha256ripemd160( privateKey.publicKey.toBuffer(), ); + console.log("privateKey", privateKey); for (let i = 0; i < tx.inputs.length; i++) { const input = tx.inputs[i]; @@ -290,6 +338,13 @@ export class WalletService { undefined, undefined, ); + console.log( + "i: ", + i, + "signatures: ", + signatures, + " isWitnessPublicKeyHashOut", + ); tx.applySignature(signatures[0]); } else if (input.output.script.isTaproot() && !input.hasWitnesses()) { @@ -302,6 +357,7 @@ export class WalletService { undefined, undefined, ); + console.log("i: ", i, "signatures: ", signatures); tx.applySignature(signatures[0]); } @@ -316,8 +372,8 @@ function derivePrivateKey( ): btc.PrivateKey { const seed = bip39.mnemonicToSeedSync(mnemonic); const mainnet = { - messagePrefix: '\x18Bitcoin Signed Message:\n', - bech32: 'bc', + messagePrefix: "\x18Bitcoin Signed Message:\n", + bech32: "bc", bip32: { public: 0x0488b21e, private: 0x0488ade4, @@ -327,8 +383,8 @@ function derivePrivateKey( wif: 0x80, }; const testnet = { - messagePrefix: '\x18Bitcoin Signed Message:\n', - bech32: 'tb', + messagePrefix: "\x18Bitcoin Signed Message:\n", + bech32: "tb", bip32: { public: 0x043587cf, private: 0x04358394, @@ -343,5 +399,6 @@ function derivePrivateKey( network === btc.Networks.mainnet ? mainnet : testnet, ); const wif = root.derivePath(path).toWIF(); + return new btc.PrivateKey(wif, network); } diff --git a/packages/cli/src/sdk.ts b/packages/cli/src/sdk.ts new file mode 100644 index 00000000..d586bf56 --- /dev/null +++ b/packages/cli/src/sdk.ts @@ -0,0 +1,328 @@ +import { UTXO } from "scrypt-ts"; +import { sendToken } from "./commands/send/ft"; +import { mergeTokens } from "./commands/send/merge"; +import { pick, pickLargeFeeUtxo } from "./commands/send/pick"; +import { + broadcast, + btc, + getTokens, + getUtxos, + logerror, + toBitcoinNetwork, + TokenMetadata, + unScaleByDecimals, +} from "./common"; +import { ConfigService, SpendService } from "./providers"; +import { WalletService } from "./providers/walletService"; + +import { Psbt, Transaction, address } from 'bitcoinjs-lib'; +import { networks } from "bitcoinjs-lib"; +import { Sequence } from "@cmdcode/tapscript"; + + +export async function send( + token: TokenMetadata, + receiver: btc.Address, + amount: bigint, + address: btc.Address, + configService: ConfigService, + walletService: WalletService, + spendService: SpendService, + feeUtxos: UTXO[], + feeRate?: number, +) { + + +} + +// export async function sendCat20( +// token: any, +// receiver: btc.Address, +// amount: string, +// senderAddress: btc.Address, +// configService: ConfigService, +// walletService: WalletService, +// spendService: SpendService, +// utxos: UTXO[], +// feeRate: number, +// ) { +// try { +// return await send( +// token, +// receiver, +// BigInt(amount), +// senderAddress, +// configService, +// walletService, +// spendService, +// utxos, +// feeRate, +// ); +// } catch (error) { +// console.error("sendTransaction -- ERROR ---", JSON.stringify(error)); +// throw new Error("Transaction failed"); +// } +// } + +interface CreateTxResult { + + commitTx: SignTransactionPayload; + revealTx: SignTransactionPayload; + + // commitTxHash: string; + // commitTxHex: string; + + // revealTxHash: string; + // revealTxHex: string; + networkFee: number; +} + +export interface InputToSign { + address: string; + signingIndexes: Array; + sigHash?: number; +} +export interface SignTransactionPayload { + // network: BitcoinNetwork; // TODO: 2525 review + message: string; + psbtBase64: string; + broadcast?: boolean; + inputsToSign: InputToSign[]; +} +export interface SignTransactionOptions { + payload: SignTransactionPayload; + onFinish: (response: any) => void; + onCancel: () => void; +} +export interface SignTransactionResponse { + psbtBase64: string; + txId?: string; +} + +const parsePsbtFromTxHex = (txHex: string, inputs: UTXO[], internalPubKeyXOnly: Buffer): Psbt => { + let psbt = new Psbt({ network: networks.bitcoin }); + + let msgTx = Transaction.fromHex(txHex); + + for (let i = 0; i < msgTx.ins.length; i++) { + let txIn = msgTx.ins[i]; + + const inputData: any = { + hash: txIn.hash, + index: txIn.index, + witnessUtxo: { value: inputs[i].satoshis, script: Buffer.from(inputs[i].script, "hex") as Buffer }, + sequence: txIn.sequence, + tapInternalKey: internalPubKeyXOnly, + }; + psbt.addInput(inputData); + } + + for (let i = 0; i < msgTx.outs.length; i++) { + let txOut = msgTx.outs[i]; + + const outputData = { + address: address.fromOutputScript(txOut.script), + value: txOut.value, + } + psbt.addOutput(outputData); + } + return psbt; +} + + + +const createRawTxSendCAT20 = async ({ + senderAddress, + receiverAddress, + amount, + token, + configService, + walletService, + spendService, + // tokenId, + feeUtxos, + feeRate, +}: { + senderAddress: string; + receiverAddress: string; + amount: bigint; + // tokenId: string; + token: TokenMetadata, + configService: ConfigService, + walletService: WalletService, + spendService: SpendService, + feeUtxos: UTXO[], + feeRate?: number, + // }) => { +}): Promise => { + + if (feeUtxos.length === 0) { + console.info("Insufficient satoshis balance!"); + return; + } + + const res = await getTokens(configService, spendService, token, senderAddress); + if (res === null) { + return; + } + + const { contracts } = res; + + let tokenContracts = pick(contracts, amount); + + if (tokenContracts.length === 0) { + console.warn("Insufficient token balance!"); + return; + } + + // TODO: 2525 NOTE: not updated yet + const cachedTxs: Map = new Map(); + if (tokenContracts.length > 4) { + console.info(`Merging your [${token.info.symbol}] tokens ...`); + const [mergedTokens, newfeeUtxos, e] = await mergeTokens( + configService, + walletService, + spendService, + feeUtxos, + feeRate, + token, + tokenContracts, + senderAddress, + cachedTxs, + ); + + if (e instanceof Error) { + logerror("merge token failed!", e); + return; + } + + tokenContracts = mergedTokens; + feeUtxos = newfeeUtxos; + } + console.log("pickLargeFeeUtxo"); + + const feeUtxo = pickLargeFeeUtxo(feeUtxos); + console.log("after pickLargeFeeUtxo"); + + const result = await sendToken( + configService, + walletService, + feeUtxo, + feeRate, + token, + tokenContracts, + senderAddress, + receiverAddress, + amount, + cachedTxs, + ); + console.log("sendToken"); + + if (result) { + // const commitTxId = await broadcast( + // configService, + // walletService, + // result.commitTx.uncheckedSerialize(), + // ); + + // if (commitTxId instanceof Error) { + // throw commitTxId; + // } + + // spendService.updateSpends(result.commitTx); + + // const revealTxId = await broadcast( + // configService, + // walletService, + // result.revealTx.uncheckedSerialize(), + // ); + + // if (revealTxId instanceof Error) { + // throw revealTxId; + // } + + // spendService.updateSpends(result.revealTx); + + console.log( + `Sending ${unScaleByDecimals(amount, token.info.decimals)} ${token.info.symbol} tokens to ${receiverAddress} \nin txid: ${result.revealTx.id}`, + ); + + + let commitTxHex = result.commitTx.uncheckedSerialize(); + let revealTxHex = result.revealTx.uncheckedSerialize(); + + console.log({ commitTxHex }); + console.log({ revealTxHex }); + + let commitPsbt = parsePsbtFromTxHex(commitTxHex, [feeUtxo], Buffer.from(walletService.getXOnlyPublicKey(), "hex")); + let commitIndicesToSign: number[] = []; + for (let i = 0; i < commitPsbt.txInputs.length; i++) { + commitIndicesToSign.push(i); + } + + let commitTx = preparePayloadSignTx({ + base64Psbt: commitPsbt.toBase64(), + indicesToSign: commitIndicesToSign, + address: senderAddress, + }) + + + let revealPsbt = parsePsbtFromTxHex(revealTxHex, [feeUtxo], Buffer.from(walletService.getXOnlyPublicKey(), "hex")); + // let revealPsbt = Psbt.fromHex(unpadRevealTxHex, { network: networks.bitcoin }); + let revealIndicesToSign: number[] = []; + for (let i = 0; i < revealPsbt.txInputs.length; i++) { + revealIndicesToSign.push(i); + } + + let revealTx = preparePayloadSignTx({ + base64Psbt: revealPsbt.toBase64(), + indicesToSign: revealIndicesToSign, + address: senderAddress, + }) + + const networkFee = result.commitTx.getFee() + result.revealTx.getFee(); + + const finalRes: CreateTxResult = { + commitTx, + revealTx, + networkFee, + } + + return finalRes; + } + return null; +}; + + +const preparePayloadSignTx = ({ + base64Psbt, + indicesToSign, + address, + sigHashType = btc.Signature.SIGHASH_DEFAULT +}: { + base64Psbt: string, + indicesToSign: number[], + address: string, + sigHashType?: number, +}): SignTransactionPayload => { + + return { + // network: { + // type: "Mainnet", + // address: "", // TODO: + // }, + message: "Sign Transaction", + psbtBase64: base64Psbt, + broadcast: false, + inputsToSign: [{ + address: address, + signingIndexes: indicesToSign, + sigHash: sigHashType, + }], + }; +}; + + +export { + createRawTxSendCAT20, +} diff --git a/packages/cli/src/sendCat20Handler.ts b/packages/cli/src/sendCat20Handler.ts new file mode 100644 index 00000000..8cf44f06 --- /dev/null +++ b/packages/cli/src/sendCat20Handler.ts @@ -0,0 +1,250 @@ +import { UTXO } from "scrypt-ts"; +import { sendToken } from "./commands/send/ft"; +import { mergeTokens } from "./commands/send/merge"; +import { pick, pickLargeFeeUtxo } from "./commands/send/pick"; +import { + broadcast, + btc, + getTokens, + getUtxos, + logerror, + TokenMetadata, + unScaleByDecimals, +} from "./common"; +import { ConfigService, SpendService } from "./providers"; +import { WalletService } from "./providers/walletService"; + + +export async function send( + token: TokenMetadata, + receiver: btc.Address, + amount: bigint, + address: btc.Address, + configService: ConfigService, + walletService: WalletService, + spendService: SpendService, + feeUtxos: UTXO[], + isBroadcast: boolean, + feeRate?: number, + +) { + // const feeRate = await this.getFeeRate(); + + // let feeUtxos = await getUtxos(configService, walletService, address); + + // console.log("========feeUtxos ori+++++++"); + // for (const utxo of feeUtxos) { + // console.log("utxo: ", utxo); + // } + + // feeUtxos = feeUtxos.filter((utxo) => { + // return spendService.isUnspent(utxo); + // }); + + if (feeUtxos.length === 0) { + console.log("Insufficient satoshis balance!"); + // throw new Error("Insufficient satoshis balance!"); + return { errorCode: '-1000', result: null}; + } + + const res = await getTokens(configService, spendService, token, address); + + if (res === null) { + return { errorCode: '-9999', errorMsg: "getTokens nil", result: null }; + } + + const { contracts } = res; + + let tokenContracts = pick(contracts, amount); + + console.log("Picked tokenContracts: ", tokenContracts.length, contracts.length); + + if (tokenContracts.length === 0) { + console.log("Insufficient token balance!"); + // throw new Error("Insufficient token balance!"); + return { errorCode: '-1001', result: null }; + } + + const cachedTxs: Map = new Map(); + if (tokenContracts.length > 4) { + console.info(`Merging your [${token.info.symbol}] tokens ...`); + const [mergedTokens, newfeeUtxos, e] = await mergeTokens( + configService, + walletService, + spendService, + feeUtxos, + feeRate, + token, + tokenContracts, + address, + cachedTxs, + ); + + if (e instanceof Error) { + console.info("merge token failed!", e); + // throw new Error("merge token failed! " + e); + return { errorCode: '-1002', errorMsg: e, result: null }; + } + + tokenContracts = mergedTokens; + feeUtxos = newfeeUtxos; + } + console.log("pickLargeFeeUtxo"); + + const feeUtxo = pickLargeFeeUtxo(feeUtxos); + console.log("after pickLargeFeeUtxo"); + + const result = await sendToken( + configService, + walletService, + feeUtxo, + feeRate, + token, + tokenContracts, + address, + receiver, + amount, + cachedTxs, + ); + console.log("sendToken"); + + if (result) { + if (isBroadcast) { + const commitTxId = await broadcast( + configService, + walletService, + result.commitTx.uncheckedSerialize(), + ); + + if (commitTxId instanceof Error) { + throw commitTxId; + } + + spendService.updateSpends(result.commitTx); + + const revealTxId = await broadcast( + configService, + walletService, + result.revealTx.uncheckedSerialize(), + ); + + if (revealTxId instanceof Error) { + throw revealTxId; + } + + spendService.updateSpends(result.revealTx); + } + + console.log( + `Sending ${unScaleByDecimals(amount, token.info.decimals)} ${token.info.symbol} tokens to ${receiver} \nin txid: ${result.revealTx.id}`, + ); + } + + return { result: result}; +} + + +export async function sendCat20( + token: any, + receiver: btc.Address, + amount: string, + senderAddress: btc.Address, + configService: ConfigService, + walletService: WalletService, + spendService: SpendService, + utxos: UTXO[], + isBroadcast: boolean, + feeRate: number, +) { + try { + const result = await send( + token, + receiver, + BigInt(amount), + senderAddress, + configService, + walletService, + spendService, + utxos, + isBroadcast, + feeRate, + ); + + if (result.errorCode) { + return { errorCode: result.errorCode, errorMsg: result.errorMsg}; + } + + return {result: result.result}; + + + } catch (error) { + console.log("sendCat20 -- ERROR ---", error); + // throw error; + return { errorCode: '-9999', errorMsg: error}; + + } +} + + +export async function estFeeSendCat20( + token: any, + amount: string, + senderAddress: btc.Address, + configService: ConfigService, + spendService: SpendService, +) { + try { + return await estCAT20UTXOs( + token, + BigInt(amount), + senderAddress, + configService, + spendService, + ); + } catch (error) { + console.log("estFeeSendCat20 -- ERROR ---", error); + throw error; + } +} + + +export async function estCAT20UTXOs( + token: TokenMetadata, + amount: bigint, + address: btc.Address, + configService: ConfigService, + spendService: SpendService, +) { + // const feeRate = await this.getFeeRate(); + + // let feeUtxos = await getUtxos(configService, walletService, address); + + // console.log("========feeUtxos ori+++++++"); + // for (const utxo of feeUtxos) { + // console.log("utxo: ", utxo); + // } + + // feeUtxos = feeUtxos.filter((utxo) => { + // return spendService.isUnspent(utxo); + // }); + + // if (feeUtxos.length === 0) { + // console.log("Insufficient satoshis balance!"); + // return; + // } + + const res = await getTokens(configService, spendService, token, address); + if (res === null) { + throw new Error("List token contract is empty"); + } + + const { contracts } = res; + let tokenContracts = pick(contracts, amount); + console.log("estCAT20UTXOs Picked tokenContracts: ", tokenContracts.length, contracts.length); + + return tokenContracts.length; +} + + + + diff --git a/packages/cli/src/token/token.ts b/packages/cli/src/token/token.ts index bd207cea..15d8325a 100644 --- a/packages/cli/src/token/token.ts +++ b/packages/cli/src/token/token.ts @@ -1,5 +1,5 @@ -import { existsSync, readFileSync, writeFileSync } from 'fs'; -import { join } from 'path'; +import { existsSync, readFileSync, writeFileSync } from "fs"; +import { join } from "path"; import { OpenMinterTokenInfo, TokenMetadata, @@ -7,8 +7,8 @@ import { scaleByDecimals, getTokenMetadata, logerror, -} from 'src/common'; -import { ConfigService } from 'src/providers'; +} from "../common"; +import { ConfigService } from "../providers"; export function scaleConfig(config: OpenMinterTokenInfo): OpenMinterTokenInfo { const clone = Object.assign({}, config); @@ -41,7 +41,7 @@ export function getAllTokenMetadatas(config: ConfigService): TokenMetadata[] { return []; } } catch (error) { - logerror('getAllTokenMetadatas failed!', error); + logerror("getAllTokenMetadatas failed!", error); } return []; @@ -52,6 +52,11 @@ export async function findTokenMetadataById( id: string, ): Promise { const tokens = getAllTokenMetadatas(config); + console.log("id: ", id); + // for (const token of tokens) { + // console.log("tokens: ", token); + // } + let token = tokens.find((token) => token.tokenId === id); if (token) { return token; @@ -76,7 +81,7 @@ function saveTokenMetadata( try { writeFileSync(path, JSON.stringify(tokens, null, 1)); } catch (error) { - console.error('save token metadata error:', error); + console.error("save token metadata error:", error); } return tokens; @@ -105,5 +110,5 @@ export function addTokenMetadata( } export function getTokenMetadataPath(config: ConfigService) { - return join(config.getDataDir(), 'tokens.json'); + return join(config.getDataDir(), "tokens.json"); } diff --git a/packages/cli/tsconfig.build.json b/packages/cli/tsconfig.build.json index 1d7acd89..e89e781d 100644 --- a/packages/cli/tsconfig.build.json +++ b/packages/cli/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] + "exclude": ["node_modules", "test", "dist", "**/*spec.ts", "index.ts"] } diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index ddd698cf..c4870f1d 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -16,6 +16,11 @@ "noImplicitAny": false, "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false + "noFallthroughCasesInSwitch": false, + }, + "ts-node": { + "require": [ + "tsconfig-paths/register" + ] } -} +} \ No newline at end of file diff --git a/tsconfig.base.json b/tsconfig.base.json index 37ac0cc8..b4b1395d 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -8,7 +8,9 @@ "declarationMap": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, - "lib": ["ESNext"], + "lib": [ + "ESNext" + ], "module": "CommonJS", "allowJs": true, "moduleResolution": "node",