UNPKG

meta-contract-debug

Version:

Meta Contract SDK

988 lines 65.9 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.FtManager = exports.sighashType = void 0; const scryptlib_1 = require("../scryptlib"); const error_1 = require("../common/error"); const mvc = require("../mvc"); const __1 = require(".."); const constants_1 = require("./constants"); const BN = require("../bn.js"); const TokenUtil = require("../common/tokenUtil"); const $ = require("../common/argumentCheck"); const Prevouts_1 = require("../common/Prevouts"); const tx_composer_1 = require("../tx-composer"); const token_1 = require("./contract-factory/token"); const contractUtil_1 = require("./contractUtil"); const utils_1 = require("../common/utils"); const tokenGenesis_1 = require("./contract-factory/tokenGenesis"); const tokenTransferCheck_1 = require("./contract-factory/tokenTransferCheck"); const ftProto = require("./contract-proto/token.proto"); const DustCalculator_1 = require("../common/DustCalculator"); const SizeTransaction_1 = require("../common/SizeTransaction"); const transactionHelpers_1 = require("../helpers/transactionHelpers"); const contractHelpers_1 = require("../helpers/contractHelpers"); const dummy_1 = require("../common/dummy"); const protoheader_1 = require("../common/protoheader"); const jsonDescr = require('./contract-desc/txUtil_desc.json'); const { TxInputProof, TxOutputProof } = (0, scryptlib_1.buildTypeClasses)(jsonDescr); const Signature = mvc.crypto.Signature; const _ = mvc.deps._; exports.sighashType = Signature.SIGHASH_ALL | Signature.SIGHASH_FORKID; contractUtil_1.ContractUtil.init(); function checkParamGenesis(genesis) { $.checkArgument(_.isString(genesis), 'Invalid Argument: genesis should be a string'); $.checkArgument(genesis.length == 40, `Invalid Argument: genesis.length must be 40`); } function checkParamCodehash(codehash) { $.checkArgument(_.isString(codehash), 'Invalid Argument: codehash should be a string'); $.checkArgument(codehash.length == 40, `Invalid Argument: codehash.length must be 40`); $.checkArgument(codehash == contractUtil_1.ContractUtil.tokenCodeHash || codehash == contractUtil_1.ContractUtil.tokenGenesisCodeHash || codehash === '57344f46cc0d0c8dfea7af3300b1b3a0f4216c04' || codehash === 'a2421f1e90c6048c36745edd44fad682e8644693', `a valid codehash should be ${contractUtil_1.ContractUtil.tokenCodeHash}, but the provided is ${codehash} `); } function checkParamReceivers(receivers) { const ErrorName = 'ReceiversFormatError'; if ((0, utils_1.isNull)(receivers)) { throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, `${ErrorName}: param should not be null`); } if (receivers.length > 0) { let receiver = receivers[0]; if ((0, utils_1.isNull)(receiver.address) || (0, utils_1.isNull)(receiver.amount)) { throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, `${ErrorName}-valid format example [ { address: "mtjjuRuA84b2qVyo28AyJQ8AoUmpbWEqs3", amount: "1000", }, ] `); } let amount = new BN(receiver.amount.toString()); if (amount.lten(0)) { throw `receiver amount must greater than 0 but now is ${receiver.amount}`; } } } function parseSensibleID(sensibleID) { let sensibleIDBuf = Buffer.from(sensibleID, 'hex'); let genesisTxId = sensibleIDBuf.slice(0, 32).reverse().toString('hex'); let genesisOutputIndex = sensibleIDBuf.readUIntLE(32, 4); return { genesisTxId, genesisOutputIndex, }; } class FtManager { constructor({ network = __1.API_NET.MAIN, apiTarget = __1.API_TARGET.MVC, purse, feeb = constants_1.FEEB, apiHost, dustLimitFactor = 300, dustAmount, debug = false, }) { // 初始化API this.network = network; this._api = new __1.Api(network, apiTarget, apiHost); // 初始化钱包 if (purse) { const privateKey = mvc.PrivateKey.fromWIF(purse); const address = privateKey.toAddress(network); this.purse = { privateKey, address, }; } // 初始化零地址 this.zeroAddress = new mvc.Address('1111111111111111111114oLvT2'); this.dustCalculator = new DustCalculator_1.DustCalculator(dustLimitFactor, dustAmount); this.transferCheckCodeHashArray = contractUtil_1.ContractUtil.transferCheckCodeHashArray; this.unlockContractCodeHashArray = contractUtil_1.ContractUtil.unlockContractCodeHashArray; // 初始化费率 this.feeb = feeb; this.debug = false; } get api() { return this._api; } get sensibleApi() { return this._api; } /** * Create a transaction for genesis * @param tokenName token name, limited to 20 bytes * @param tokenSymbol the token symbol, limited to 10 bytes * @param decimalNum the decimal number, range 0-255 * @param utxos (Optional) specify mvc utxos * @param changeAddress (Optional) specify mvc changeAddress * @param opreturnData (Optional) append an opReturn output * @param genesisWif the private key of the token genesiser * @param noBroadcast (Optional) whether not to broadcast the transaction, the default is false * @returns */ genesis({ tokenName, tokenSymbol, decimalNum, utxos: utxosInput, changeAddress, opreturnData, genesisWif, noBroadcast = false, }) { return __awaiter(this, void 0, void 0, function* () { // TODO 检查必要参数 // validate params $.checkArgument(_.isString(tokenName) && Buffer.from(tokenName).length <= 40, `tokenName should be a string and not be larger than 40 bytes`); $.checkArgument(_.isString(tokenSymbol) && Buffer.from(tokenSymbol).length <= 10, 'tokenSymbol should be a string and not be larger than 10 bytes'); $.checkArgument(_.isNumber(decimalNum) && decimalNum >= 0 && decimalNum <= 255, 'decimalNum should be a number and must be between 0 and 255'); const utxoInfo = yield (0, transactionHelpers_1.prepareUtxos)(this.purse, this.api, this.network, utxosInput); if (changeAddress) { changeAddress = new mvc.Address(changeAddress, this.network); } else { changeAddress = utxoInfo.utxos[0].address; } const tokenAddress = genesisWif ? mvc.PrivateKey.fromWIF(genesisWif).toAddress(this.network) : this.purse.address; let { txComposer } = yield this._genesis({ tokenName, tokenSymbol, decimalNum, utxos: utxoInfo.utxos, utxoPrivateKeys: utxoInfo.utxoPrivateKeys, changeAddress: changeAddress, tokenAddress: tokenAddress.hashBuffer.toString('hex'), opreturnData, }); let txHex = txComposer.getRawHex(); if (!noBroadcast) { yield this.api.broadcast(txHex); } let { codehash, genesis, sensibleId } = (0, contractHelpers_1.getGenesisIdentifiers)({ genesisTx: txComposer.getTx(), purse: { address: tokenAddress, privateKey: this.purse.privateKey }, transferCheckCodeHashArray: this.transferCheckCodeHashArray, unlockContractCodeHashArray: this.unlockContractCodeHashArray, type: 'ft', }); return { txHex, txid: txComposer.getTxId(), tx: txComposer.getTx(), codehash, genesis, sensibleId, }; }); } issue(options) { return __awaiter(this, void 0, void 0, function* () { return this.mint(options); }); } mint({ // genesis, // codehash, sensibleId, genesisWif, receiverAddress, tokenAmount, allowIncreaseMints = true, utxos: utxosInput, changeAddress, opreturnData, noBroadcast = false, }) { return __awaiter(this, void 0, void 0, function* () { // checkParamGenesis(genesis) // checkParamCodehash(codehash) $.checkArgument(sensibleId, 'sensibleId is required'); $.checkArgument(genesisWif, 'genesisWif is required'); $.checkArgument(receiverAddress, 'receiverAddress is required'); $.checkArgument(tokenAmount, 'tokenAmount is required'); const utxoInfo = yield (0, transactionHelpers_1.prepareUtxos)(this.purse, this.api, this.network, utxosInput); if (changeAddress) { changeAddress = new mvc.Address(changeAddress, this.network); } else { changeAddress = utxoInfo.utxos[0].address; } let genesisPrivateKey = new mvc.PrivateKey(genesisWif); let genesisPublicKey = genesisPrivateKey.toPublicKey(); receiverAddress = new mvc.Address(receiverAddress, this.network); tokenAmount = new BN(tokenAmount.toString()); let { txComposer } = yield this._mint({ // genesis, // codehash, sensibleId, receiverAddress, tokenAmount, allowIncreaseMints, utxos: utxoInfo.utxos, utxoPrivateKeys: utxoInfo.utxoPrivateKeys, changeAddress, opreturnData, genesisPrivateKey, genesisPublicKey, }); let txHex = txComposer.getRawHex(); if (!noBroadcast) { yield this.api.broadcast(txHex); } return { txHex, txid: txComposer.getTxId(), tx: txComposer.getTx() }; }); } _mint({ // genesis, // codehash, sensibleId, receiverAddress, tokenAmount, allowIncreaseMints = true, utxos, utxoPrivateKeys, changeAddress, opreturnData, genesisPrivateKey, genesisPublicKey, }) { return __awaiter(this, void 0, void 0, function* () { const genesisAddress = genesisPrivateKey.toAddress(this.network).toString(); let { genesisContract, genesisTxId, genesisOutputIndex, genesisUtxo } = yield this._prepareMintUtxo({ sensibleId, genesisAddress }); let balance = utxos.reduce((pre, cur) => pre + cur.satoshis, 0); let estimateSatoshis = yield this._calMintEstimateFee({ genesisUtxoSatoshis: genesisUtxo.satoshis, opreturnData, allowIncreaseMints, utxoMaxCount: utxos.length, }); if (balance < estimateSatoshis) { throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_BSV, `Insufficient balance.It take more than ${estimateSatoshis}, but only ${balance}.`); } let newGenesisContract = genesisContract.clone(); newGenesisContract.setFormatedDataPart({ sensibleID: { txid: genesisTxId, index: genesisOutputIndex, }, }); let tokenContract = token_1.TokenFactory.createContract(this.transferCheckCodeHashArray, this.unlockContractCodeHashArray); tokenContract.setFormatedDataPart(Object.assign({}, newGenesisContract.getFormatedDataPart(), { tokenAddress: (0, scryptlib_1.toHex)(receiverAddress.hashBuffer), tokenAmount, genesisHash: newGenesisContract.getScriptHash(), })); const txComposer = new tx_composer_1.TxComposer(); const genesisInputIndex = (0, transactionHelpers_1.addContractInput)(txComposer, genesisUtxo, genesisPublicKey.toAddress(this.network).toString(), utils_1.CONTRACT_TYPE.BCP02_TOKEN_GENESIS); const p2pkhInputIndexs = (0, transactionHelpers_1.addP2PKHInputs)(txComposer, utxos); //If increase issues is allowed, add a new issue contract as the first output let newGenesisOutputIndex = -1; if (allowIncreaseMints) { newGenesisOutputIndex = (0, transactionHelpers_1.addContractOutput)({ txComposer, contract: newGenesisContract, dustCalculator: this.dustCalculator, }); } const tokenOutputIndex = (0, transactionHelpers_1.addContractOutput)({ txComposer, contract: tokenContract, dustCalculator: this.dustCalculator, }); //If there is opReturn, add it to the output let opreturnScriptHex = ''; if (opreturnData) { const opreturnOutputIndex = (0, transactionHelpers_1.addOpreturnOutput)(txComposer, opreturnData); opreturnScriptHex = txComposer.getOutput(opreturnOutputIndex).script.toHex(); } const prevInputIndex = 0; // TODO: 0? const genesisTx = genesisUtxo.satotxInfo.tx; const inputRes = TokenUtil.getTxInputProof(genesisTx, prevInputIndex); const genesisTxInputProof = new TxInputProof(inputRes[0]); const genesisTxHeader = inputRes[1]; // TODO: // Find a valid preGenesisTx const genesisTxInput = genesisTx.inputs[prevInputIndex]; const preGenesisOutputIndex = genesisTxInput.outputIndex; const preGenesisTxId = genesisTxInput.prevTxId.toString('hex'); const preGenesisTxHex = yield this.api.getRawTxData(preGenesisTxId); const preGenesisTx = new mvc.Transaction(preGenesisTxHex); const prevOutputProof = TokenUtil.getTxOutputProof(preGenesisTx, preGenesisOutputIndex); const pubKey = new scryptlib_1.PubKey(genesisPublicKey.toHex()); //The first round of calculations get the exact size of the final transaction, and then change again //Due to the change, the script needs to be unlocked again in the second round //let the fee to be exact in the second round for (let c = 0; c < 2; c++) { // TODO: 取消两轮? txComposer.clearChangeOutput(); const changeOutputIndex = txComposer.appendChangeOutput(changeAddress, this.feeb); let unlockResult = genesisContract.unlock({ txPreimage: txComposer.getInputPreimage(genesisInputIndex), pubKey, sig: new scryptlib_1.Sig(genesisPrivateKey ? (0, scryptlib_1.toHex)(txComposer.getTxFormatSig(genesisPrivateKey, genesisInputIndex)) : utils_1.PLACE_HOLDER_SIG), tokenScript: new scryptlib_1.Bytes(txComposer.getOutput(tokenOutputIndex).script.toHex()), // GenesisTx Input Proof genesisTxHeader, prevInputIndex, genesisTxInputProof, // Prev GenesisTx Output Proof prevGenesisTxHeader: prevOutputProof.txHeader, prevTxOutputHashProof: prevOutputProof.hashProof, prevTxOutputSatoshiBytes: prevOutputProof.satoshiBytes, genesisSatoshis: newGenesisOutputIndex != -1 ? txComposer.getOutput(newGenesisOutputIndex).satoshis : 0, tokenSatoshis: txComposer.getOutput(tokenOutputIndex).satoshis, changeSatoshis: changeOutputIndex != -1 ? txComposer.getOutput(changeOutputIndex).satoshis : 0, changeAddress: new scryptlib_1.Ripemd160((0, scryptlib_1.toHex)(changeAddress.hashBuffer)), opReturnScript: new scryptlib_1.Bytes(opreturnScriptHex), }); // const txContext = { // tx: txComposer.getTx(), // inputIndex: 0, // inputSatoshis: txComposer.getOutput(newGenesisOutputIndex).satoshis, // } // const verify = unlockResult.verify(txContext) // console.log({ verify }) if (this.debug && genesisPrivateKey && c == 1) { let ret = unlockResult.verify({ tx: txComposer.tx, inputIndex: genesisInputIndex, inputSatoshis: txComposer.getInput(genesisInputIndex).output.satoshis, }); if (ret.success == false) throw ret; } txComposer.getInput(genesisInputIndex).setScript(unlockResult.toScript()); } (0, transactionHelpers_1.unlockP2PKHInputs)(txComposer, p2pkhInputIndexs, utxoPrivateKeys); // if (utxoPrivateKeys && utxoPrivateKeys.length > 0) { // p2pkhInputIndexs.forEach((inputIndex) => { // let privateKey = utxoPrivateKeys.splice(0, 1)[0] // txComposer.unlockP2PKHInput(privateKey, inputIndex) // }) // } (0, transactionHelpers_1.checkFeeRate)(txComposer, this.feeb); return { txComposer }; }); } _prepareMintUtxo({ sensibleId, genesisAddress, }) { return __awaiter(this, void 0, void 0, function* () { let genesisContract = tokenGenesis_1.TokenGenesisFactory.createContract(); //Looking for UTXO for issue let { genesisTxId, genesisOutputIndex } = parseSensibleID(sensibleId); let genesisUtxo = yield this._getMintUtxo(genesisContract.getCodeHash(), genesisTxId, genesisOutputIndex, genesisAddress); if (!genesisUtxo) { throw new error_1.CodeError(error_1.ErrCode.EC_FIXED_TOKEN_SUPPLY, 'token supply is fixed'); } let txHex = yield this.api.getRawTxData(genesisUtxo.txId); const tx = new mvc.Transaction(txHex); let preTxId = tx.inputs[0].prevTxId.toString('hex'); let preOutputIndex = tx.inputs[0].outputIndex; let preTxHex = yield this.api.getRawTxData(preTxId); genesisUtxo.satotxInfo = { txId: genesisUtxo.txId, outputIndex: genesisUtxo.outputIndex, txHex, preTxId, preOutputIndex, preTxHex, tx, }; let output = tx.outputs[genesisUtxo.outputIndex]; genesisUtxo.satoshis = output.satoshis; genesisUtxo.lockingScript = output.script; genesisContract.setFormatedDataPartFromLockingScript(genesisUtxo.lockingScript); return { genesisContract, genesisTxId, genesisOutputIndex, genesisUtxo, }; }); } _getMintUtxo(codehash, genesisTxId, genesisOutputIndex, genesisAddress) { return __awaiter(this, void 0, void 0, function* () { let unspent; let firstGenesisTxHex = yield this.api.getRawTxData(genesisTxId); let firstGenesisTx = new mvc.Transaction(firstGenesisTxHex); let scriptBuffer = firstGenesisTx.outputs[genesisOutputIndex].script.toBuffer(); let originGenesis = ftProto.getQueryGenesis(scriptBuffer); let genesisUtxos = yield this.api.getFungibleTokenUnspents(codehash, originGenesis, genesisAddress); unspent = genesisUtxos.find((v) => v.txId == genesisTxId && v.outputIndex == genesisOutputIndex); if (!unspent) { let _dataPartObj = ftProto.parseDataPart(scriptBuffer); _dataPartObj.sensibleID = { txid: genesisTxId, index: genesisOutputIndex, }; let newScriptBuf = ftProto.updateScript(scriptBuffer, _dataPartObj); let issueGenesis = ftProto.getQueryGenesis(newScriptBuf); let issueUtxos = yield this.api.getFungibleTokenUnspents(codehash, issueGenesis, genesisAddress); if (issueUtxos.length > 0) { unspent = issueUtxos[0]; } } if (unspent) { return { txId: unspent.txId, outputIndex: unspent.outputIndex, }; } }); } _calMintEstimateFee({ genesisUtxoSatoshis, opreturnData, allowIncreaseMints = true, utxoMaxCount = 10, }) { return __awaiter(this, void 0, void 0, function* () { let p2pkhInputNum = utxoMaxCount; let stx = new SizeTransaction_1.SizeTransaction(this.feeb, this.dustCalculator); stx.addInput(tokenGenesis_1.TokenGenesisFactory.calUnlockingScriptSize(opreturnData), genesisUtxoSatoshis); for (let i = 0; i < p2pkhInputNum; i++) { stx.addP2PKHInput(); } if (allowIncreaseMints) { stx.addOutput(tokenGenesis_1.TokenGenesisFactory.getLockingScriptSize()); } stx.addOutput(token_1.TokenFactory.getLockingScriptSize()); if (opreturnData) { stx.addOpReturnOutput(mvc.Script.buildSafeDataOut(opreturnData).toBuffer().length); } stx.addP2PKHOutput(); return stx.getFee(); }); } merge({ codehash, genesis, ownerWif, utxos, changeAddress, noBroadcast = false, opreturnData, }) { return __awaiter(this, void 0, void 0, function* () { $.checkArgument(ownerWif, 'ownerWif is required'); return yield this.transfer({ codehash, genesis, senderWif: ownerWif, utxos, changeAddress, isMerge: true, noBroadcast, receivers: [], opreturnData, }); }); } _pretreatUtxos(paramUtxos) { return __awaiter(this, void 0, void 0, function* () { let utxoPrivateKeys = []; let utxos = []; //If utxos are not provided, use purse to fetch utxos if (!paramUtxos) { if (!this.purse) throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, 'Utxos or Purse must be provided.'); paramUtxos = yield this.api.getUnspents(this.purse.address.toString()); paramUtxos.forEach((v) => { utxoPrivateKeys.push(this.purse.privateKey); }); } else { paramUtxos.forEach((v) => { if (v.wif) { let privateKey = new mvc.PrivateKey(v.wif); utxoPrivateKeys.push(privateKey); v.address = privateKey.toAddress(this.network).toString(); //Compatible with the old version, only wif is provided but no address is provided } }); } paramUtxos.forEach((v) => { utxos.push({ txId: v.txId, outputIndex: v.outputIndex, satoshis: v.satoshis, address: new mvc.Address(v.address, this.network), }); }); if (utxos.length == 0) throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_BSV, 'Insufficient balance.'); return { utxos, utxoPrivateKeys }; }); } /** * Estimate the cost of genesis * @param opreturnData * @param utxoMaxCount Maximum number of BSV UTXOs supported * @returns */ getGenesisEstimateFee({ opreturnData, utxoMaxCount = 10, }) { return __awaiter(this, void 0, void 0, function* () { const p2pkhInputNum = utxoMaxCount; const sizeOfTokenGenesis = tokenGenesis_1.TokenGenesisFactory.getLockingScriptSize(); let stx = new SizeTransaction_1.SizeTransaction(this.feeb, this.dustCalculator); for (let i = 0; i < p2pkhInputNum; i++) { stx.addP2PKHInput(); } stx.addOutput(sizeOfTokenGenesis); if (opreturnData) { stx.addOpReturnOutput(mvc.Script.buildSafeDataOut(opreturnData).toBuffer().length); } stx.addP2PKHOutput(); return stx.getFee(); }); } _genesis({ tokenName, tokenSymbol, decimalNum, utxos, utxoPrivateKeys, changeAddress, tokenAddress, opreturnData, }) { return __awaiter(this, void 0, void 0, function* () { //create genesis contract let genesisContract = tokenGenesis_1.TokenGenesisFactory.createContract(); genesisContract.setFormatedDataPart({ tokenName, tokenSymbol, decimalNum, tokenAddress, }); let estimateSatoshis = yield this.getGenesisEstimateFee({ opreturnData, utxoMaxCount: utxos.length, }); const balance = utxos.reduce((pre, cur) => pre + cur.satoshis, 0); if (balance < estimateSatoshis) { throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_BSV, `Insufficient balance.It take more than ${estimateSatoshis}, but only ${balance}.`); } const txComposer = new tx_composer_1.TxComposer(); const p2pkhInputIndexs = (0, transactionHelpers_1.addP2PKHInputs)(txComposer, utxos); (0, transactionHelpers_1.addContractOutput)({ txComposer, contract: genesisContract, dustCalculator: this.dustCalculator, }); //If there is opReturn, add it to the second output if (opreturnData) { txComposer.appendOpReturnOutput(opreturnData); } (0, transactionHelpers_1.addChangeOutput)(txComposer, changeAddress, this.feeb); (0, transactionHelpers_1.unlockP2PKHInputs)(txComposer, p2pkhInputIndexs, utxoPrivateKeys); (0, transactionHelpers_1.checkFeeRate)(txComposer, this.feeb); return { txComposer }; }); } transfer({ codehash, genesis, receivers, senderWif, ftUtxos, ftChangeAddress, utxos, changeAddress, middleChangeAddress, middlePrivateKey, minUtxoSet = true, isMerge, opreturnData, noBroadcast = false, }) { return __awaiter(this, void 0, void 0, function* () { checkParamGenesis(genesis); checkParamCodehash(codehash); checkParamReceivers(receivers); let senderPrivateKey; let senderPublicKey; if (senderWif) { senderPrivateKey = new mvc.PrivateKey(senderWif); } let utxoInfo = yield this._pretreatUtxos(utxos); if (changeAddress) { changeAddress = new mvc.Address(changeAddress, this.network); } else { changeAddress = utxoInfo.utxos[0].address; } if (middleChangeAddress) { middleChangeAddress = new mvc.Address(middleChangeAddress, this.network); middlePrivateKey = new mvc.PrivateKey(middlePrivateKey); } else { middleChangeAddress = utxoInfo.utxos[0].address; middlePrivateKey = utxoInfo.utxoPrivateKeys[0]; } let ftUtxoInfo = yield this._pretreatFtUtxos(ftUtxos, codehash, genesis, senderPrivateKey, senderPublicKey); if (ftChangeAddress) { ftChangeAddress = new mvc.Address(ftChangeAddress, this.network); } else { ftChangeAddress = ftUtxoInfo.ftUtxos[0].tokenAddress; } console.log({ ftUtxoInfo }); let { txComposer, transferCheckTxComposer } = yield this._transfer({ codehash, genesis, receivers, ftUtxos: ftUtxoInfo.ftUtxos, ftPrivateKeys: ftUtxoInfo.ftUtxoPrivateKeys, ftChangeAddress, utxos: utxoInfo.utxos, utxoPrivateKeys: utxoInfo.utxoPrivateKeys, changeAddress, opreturnData, isMerge, middleChangeAddress, middlePrivateKey, minUtxoSet, }); let routeCheckTxHex = transferCheckTxComposer.getRawHex(); let txHex = txComposer.getRawHex(); const inSats = transferCheckTxComposer .getTx() .inputs.reduce((pre, input) => pre + input.output.satoshis, 0); const outSats = transferCheckTxComposer .getTx() .outputs.reduce((pre, output) => pre + output.satoshis, 0); if (!noBroadcast) { yield this.api.broadcast(routeCheckTxHex); yield this.api.broadcast(txHex); } return { tx: txComposer.getTx(), txHex, routeCheckTx: transferCheckTxComposer.getTx(), routeCheckTxHex, txid: txComposer.getTxId(), }; }); } _pretreatFtUtxos(paramFtUtxos, codehash, genesis, senderPrivateKey, senderPublicKey) { return __awaiter(this, void 0, void 0, function* () { let ftUtxos = []; let ftUtxoPrivateKeys = []; let publicKeys = []; if (!paramFtUtxos) { if (senderPrivateKey) { senderPublicKey = senderPrivateKey.toPublicKey(); } if (!senderPublicKey) throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, 'ftUtxos or senderPublicKey or senderPrivateKey must be provided.'); paramFtUtxos = yield this.api.getFungibleTokenUnspents(codehash, genesis, senderPublicKey.toAddress(this.network).toString(), 20); paramFtUtxos.forEach((v) => { if (senderPrivateKey) { ftUtxoPrivateKeys.push(senderPrivateKey); } publicKeys.push(senderPublicKey); }); } else { paramFtUtxos.forEach((v) => { if (v.wif) { let privateKey = new mvc.PrivateKey(v.wif); ftUtxoPrivateKeys.push(privateKey); publicKeys.push(privateKey.toPublicKey()); } }); } paramFtUtxos.forEach((v, index) => { ftUtxos.push({ txId: v.txId, outputIndex: v.outputIndex, tokenAddress: new mvc.Address(v.tokenAddress, this.network), tokenAmount: new BN(v.tokenAmount.toString()), publicKey: publicKeys[index], }); }); if (ftUtxos.length == 0) throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_FT, 'Insufficient token.'); return { ftUtxos, ftUtxoPrivateKeys }; }); } _prepareTransferTokens({ codehash, genesis, receivers, ftUtxos, ftChangeAddress, isMerge, minUtxoSet, }) { return __awaiter(this, void 0, void 0, function* () { let mergeUtxos = []; let mergeTokenAmountSum = BN.Zero; if (isMerge) { mergeUtxos = ftUtxos.slice(0, 20); mergeTokenAmountSum = mergeUtxos.reduce((pre, cur) => cur.tokenAmount.add(pre), BN.Zero); receivers = [ { address: ftChangeAddress.toString(), amount: mergeTokenAmountSum.toString(), }, ]; } let tokenOutputArray = receivers.map((v) => ({ address: new mvc.Address(v.address, this.network), tokenAmount: new BN(v.amount.toString()), })); let outputTokenAmountSum = tokenOutputArray.reduce((pre, cur) => cur.tokenAmount.add(pre), BN.Zero); let inputTokenAmountSum = BN.Zero; let _ftUtxos = []; for (let i = 0; i < ftUtxos.length; i++) { let ftUtxo = ftUtxos[i]; _ftUtxos.push(ftUtxo); inputTokenAmountSum = ftUtxo.tokenAmount.add(inputTokenAmountSum); if (minUtxoSet && inputTokenAmountSum.gte(outputTokenAmountSum)) { break; } } if (isMerge) { _ftUtxos = mergeUtxos; inputTokenAmountSum = mergeTokenAmountSum; if (mergeTokenAmountSum.eq(BN.Zero)) { throw new error_1.CodeError(error_1.ErrCode.EC_INNER_ERROR, 'No utxos to merge.'); } } //Decide whether to change the token let changeTokenAmount = inputTokenAmountSum.sub(outputTokenAmountSum); if (changeTokenAmount.gt(BN.Zero)) { tokenOutputArray.push({ address: ftChangeAddress, tokenAmount: changeTokenAmount, }); } if (inputTokenAmountSum.lt(outputTokenAmountSum)) { throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_FT, `Insufficient token. Need ${outputTokenAmountSum} But only ${inputTokenAmountSum}`); } ftUtxos = _ftUtxos; yield this.perfectFtUtxosInfo(ftUtxos, codehash, genesis); let tokenInputArray = ftUtxos; //Choose a transfer plan let inputLength = tokenInputArray.length; let outputLength = tokenOutputArray.length; let tokenTransferType = tokenTransferCheck_1.TokenTransferCheckFactory.getOptimumType(inputLength, outputLength); if (tokenTransferType == tokenTransferCheck_1.TOKEN_TRANSFER_TYPE.UNSUPPORT) { throw new error_1.CodeError(error_1.ErrCode.EC_TOO_MANY_FT_UTXOS, 'Too many token-utxos, should merge them to continue.'); } // console.log({ tokenTransferType }) // let tokenTransferType = TOKEN_TRANSFER_TYPE.IN_3_OUT_3 return { tokenInputArray, tokenOutputArray, tokenTransferType, }; }); } perfectFtUtxosInfo(ftUtxos, codehash, genesis) { return __awaiter(this, void 0, void 0, function* () { //Cache txHex to prevent redundant queries let cachedHexs = {}; //Get txHex for (let i = 0; i < ftUtxos.length; i++) { let ftUtxo = ftUtxos[i]; if (!cachedHexs[ftUtxo.txId]) { cachedHexs[ftUtxo.txId] = { waitingRes: this.api.getRawTxData(ftUtxo.txId), //async request }; } } for (let id in cachedHexs) { //Wait for all async requests to complete if (cachedHexs[id].waitingRes && !cachedHexs[id].hex) { cachedHexs[id].hex = yield cachedHexs[id].waitingRes; } } ftUtxos.forEach((v) => { v.satotxInfo = v.satotxInfo || {}; v.satotxInfo.txHex = cachedHexs[v.txId].hex; v.satotxInfo.txId = v.txId; v.satotxInfo.outputIndex = v.outputIndex; }); //Get preTxHex let curDataPartObj; for (let i = 0; i < ftUtxos.length; i++) { let ftUtxo = ftUtxos[i]; const tx = new mvc.Transaction(ftUtxo.satotxInfo.txHex); if (!curDataPartObj) { let tokenScript = tx.outputs[ftUtxo.outputIndex].script; curDataPartObj = ftProto.parseDataPart(tokenScript.toBuffer()); } //Find a valid preTx let prevTokenInputIndex = 0; let input = tx.inputs.find((input, inputIndex) => { let script = new mvc.Script(input.script); if (script.chunks.length > 0) { const lockingScriptBuf = TokenUtil.getLockingScriptFromPreimage(script.chunks[0].buf); if (lockingScriptBuf) { if (ftProto.getQueryGenesis(lockingScriptBuf) == genesis) { prevTokenInputIndex = inputIndex; return true; } let dataPartObj = ftProto.parseDataPart(lockingScriptBuf); dataPartObj.sensibleID = curDataPartObj.sensibleID; const newScriptBuf = ftProto.updateScript(lockingScriptBuf, dataPartObj); let genesisHash = (0, scryptlib_1.toHex)(mvc.crypto.Hash.sha256ripemd160(newScriptBuf)); if (genesisHash == curDataPartObj.genesisHash) { prevTokenInputIndex = inputIndex; return true; } } } }); if (!input) { throw new error_1.CodeError(error_1.ErrCode.EC_INNER_ERROR, 'There is no valid preTx of the ftUtxo. '); } let preTxId = input.prevTxId.toString('hex'); let preOutputIndex = input.outputIndex; ftUtxo.satotxInfo.preTxId = preTxId; ftUtxo.satotxInfo.preOutputIndex = preOutputIndex; ftUtxo.satotxInfo.txInputsCount = tx.inputs.length; ftUtxo.satoshis = tx.outputs[ftUtxo.outputIndex].satoshis; ftUtxo.lockingScript = tx.outputs[ftUtxo.outputIndex].script; // 新增字段 prevTokenInputIndex, prevTokenOutputIndex ftUtxo.prevTokenOutputIndex = input.outputIndex; ftUtxo.prevTokenInputIndex = prevTokenInputIndex; if (!cachedHexs[preTxId]) { cachedHexs[preTxId] = { waitingRes: this.api.getRawTxData(preTxId), }; } } for (let id in cachedHexs) { //Wait for all async requests to complete if (cachedHexs[id].waitingRes && !cachedHexs[id].hex) { cachedHexs[id].hex = yield cachedHexs[id].waitingRes; } } ftUtxos.forEach((v) => { v.satotxInfo.preTxHex = cachedHexs[v.satotxInfo.preTxId].hex; const preTx = new mvc.Transaction(v.satotxInfo.preTxHex); let dataPartObj = ftProto.parseDataPart(preTx.outputs[v.satotxInfo.preOutputIndex].script.toBuffer()); v.preTokenAmount = new BN(dataPartObj.tokenAmount.toString()); if (dataPartObj.tokenAddress == '0000000000000000000000000000000000000000') { v.preTokenAddress = this.zeroAddress; } else { v.preTokenAddress = mvc.Address.fromPublicKeyHash(Buffer.from(dataPartObj.tokenAddress, 'hex'), this.network); } v.preLockingScript = preTx.outputs[v.satotxInfo.preOutputIndex].script; // 新增字段 prevTokenTx, v.prevTokenTx = preTx; }); // ftUtxos.forEach((v) => { // v.preTokenAmount = new BN(v.preTokenAmount.toString()) // }) return ftUtxos; }); } _transfer({ codehash, genesis, receivers, ftUtxos, ftPrivateKeys, ftChangeAddress, utxos, utxoPrivateKeys, changeAddress, middlePrivateKey, middleChangeAddress, isMerge, opreturnData, minUtxoSet, }) { return __awaiter(this, void 0, void 0, function* () { if (utxos.length > 3) { throw new error_1.CodeError(error_1.ErrCode.EC_UTXOS_MORE_THAN_3, 'Bsv utxos should be no more than 3 in the transfer operation, please merge it first '); } if (!middleChangeAddress) { middleChangeAddress = utxos[0].address; middlePrivateKey = utxoPrivateKeys[0]; } let { tokenInputArray, tokenOutputArray, tokenTransferType } = yield this._prepareTransferTokens({ codehash, genesis, receivers, ftUtxos, ftChangeAddress, isMerge, minUtxoSet, }); let estimateSatoshis = this._calTransferEstimateFee({ p2pkhInputNum: utxos.length, tokenInputArray, tokenOutputArray, tokenTransferType, opreturnData, }); const balance = utxos.reduce((pre, cur) => pre + cur.satoshis, 0); if (balance < estimateSatoshis) { throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_BSV, `Insufficient balance.It take more than ${estimateSatoshis}, but only ${balance}.`); } ftUtxos = tokenInputArray; const defaultFtUtxo = tokenInputArray[0]; const ftUtxoTx = new mvc.Transaction(defaultFtUtxo.satotxInfo.txHex); const tokenLockingScript = ftUtxoTx.outputs[defaultFtUtxo.outputIndex].script; //create routeCheck contract let tokenTransferCheckContract = tokenTransferCheck_1.TokenTransferCheckFactory.createContract(tokenTransferType); tokenTransferCheckContract.setFormatedDataPart({ nSenders: tokenInputArray.length, receiverTokenAmountArray: tokenOutputArray.map((v) => v.tokenAmount), receiverArray: tokenOutputArray.map((v) => v.address), nReceivers: tokenOutputArray.length, tokenCodeHash: (0, scryptlib_1.toHex)(ftProto.getContractCodeHash(tokenLockingScript.toBuffer())), tokenID: (0, scryptlib_1.toHex)(ftProto.getTokenID(tokenLockingScript.toBuffer())), }); const transferCheckTxComposer = new tx_composer_1.TxComposer(); //tx addInput utxo const transferCheck_p2pkhInputIndexs = utxos.map((utxo) => { const inputIndex = transferCheckTxComposer.appendP2PKHInput(utxo); transferCheckTxComposer.addSigHashInfo({ inputIndex, address: utxo.address.toString(), sighashType: exports.sighashType, contractType: utils_1.CONTRACT_TYPE.P2PKH, }); return inputIndex; }); const transferCheckOutputIndex = transferCheckTxComposer.appendOutput({ lockingScript: tokenTransferCheckContract.lockingScript, satoshis: this.getDustThreshold(tokenTransferCheckContract.lockingScript.toBuffer().length), }); let changeOutputIndex = transferCheckTxComposer.appendChangeOutput(middleChangeAddress, this.feeb); let unsignSigPlaceHolderSize = 0; if (utxoPrivateKeys && utxoPrivateKeys.length > 0) { transferCheck_p2pkhInputIndexs.forEach((inputIndex) => { let privateKey = utxoPrivateKeys.splice(0, 1)[0]; transferCheckTxComposer.unlockP2PKHInput(privateKey, inputIndex); }); } else { //To supplement the size calculation when unsigned transferCheck_p2pkhInputIndexs.forEach((v) => { unsignSigPlaceHolderSize += utils_1.P2PKH_UNLOCK_SIZE; }); //Each ftUtxo need to unlock with the size unsignSigPlaceHolderSize = unsignSigPlaceHolderSize * ftUtxos.length; } utxos = [ { txId: transferCheckTxComposer.getTxId(), satoshis: transferCheckTxComposer.getOutput(changeOutputIndex).satoshis, outputIndex: changeOutputIndex, address: middleChangeAddress, }, ]; utxoPrivateKeys = utxos.map((v) => middlePrivateKey).filter((v) => v); let transferCheckUtxo = { txId: transferCheckTxComposer.getTxId(), outputIndex: transferCheckOutputIndex, satoshis: transferCheckTxComposer.getOutput(transferCheckOutputIndex).satoshis, lockingScript: transferCheckTxComposer.getOutput(transferCheckOutputIndex).script, }; let transferCheckTx = transferCheckTxComposer.getTx(); const txComposer = new tx_composer_1.TxComposer(); let prevouts = new Prevouts_1.Prevouts(); let inputTokenScript; let inputTokenAmountArray = Buffer.alloc(0); let inputTokenAddressArray = Buffer.alloc(0); const ftUtxoInputIndexs = ftUtxos.map((ftUtxo) => { const inputIndex = txComposer.appendInput(ftUtxo); prevouts.addVout(ftUtxo.txId, ftUtxo.outputIndex); txComposer.addSigHashInfo({ inputIndex, address: ftUtxo.tokenAddress.toString(), sighashType: exports.sighashType, contractType: utils_1.CONTRACT_TYPE.BCP02_TOKEN, }); inputTokenScript = ftUtxo.lockingScript; inputTokenAddressArray = Buffer.concat([ inputTokenAddressArray, ftUtxo.tokenAddress.hashBuffer, ]); inputTokenAmountArray = Buffer.concat([ inputTokenAmountArray, ftUtxo.tokenAmount.toBuffer({ endian: 'little', size: 8, }), ]); return inputIndex; }); //tx addInput utxo const p2pkhInputIndexs = utxos.map((utxo) => { const inputIndex = txComposer.appendP2PKHInput(utxo); prevouts.addVout(utxo.txId, utxo.outputIndex); txComposer.addSigHashInfo({ inputIndex, address: utxo.address.toString(), sighashType: exports.sighashType, contractType: utils_1.CONTRACT_TYPE.P2PKH, }); return inputIndex; }); //添加transferCheck为最后一个输入 const transferCheckInputIndex = txComposer.appendInput(transferCheckUtxo); prevouts.addVout(transferCheckUtxo.txId, transferCheckUtxo.outputIndex); let recervierArray = Buffer.alloc(0); let receiverTokenAmountArray = Buffer.alloc(0); let outputSatoshiArray = Buffer.alloc(0); const tokenOutputLen = tokenOutputArray.length; for (let i = 0; i < tokenOutputLen; i++) { const tokenOutput = tokenOutputArray[i]; const address = tokenOutput.address; const outputTokenAmount = tokenOutput.tokenAmount; const lockingScriptBuf = ftProto.getNewTokenScript(inputTokenScript.toBuffer(), address.hashBuffer, outputTokenAmount); let outputIndex = txComposer.appendOutput({ lockingScript: mvc.Script.fromBuffer(lockingScriptBuf), satoshis: this.getDustThreshold(lockingScriptBuf.length), }); recervierArray = Buffer.concat([recervierArray, address.hashBuffer]); const tokenBuf = outputTokenAmount.toBuffer({ endian: 'little', size: 8, }); receiverTokenAmountArray = Buffer.concat([receiverTokenAmountArray, tokenBuf]); const satoshiBuf = BN.fromNumber(txComposer.getOutput(outputIndex).satoshis).toBuffer({ endian: 'little', size: 8, }); outputSatoshiArray = Buffer.concat([outputSatoshiArray, satoshiBuf]); } //tx addOutput OpReturn let opreturnScriptHex = ''; if (opreturnData) { const opreturnOutputIndex = txComposer.appendOpReturnOutput(opreturnData); opreturnScriptHex = txComposer.getOutput(opreturnOutputIndex).script.toHex(); } //The first round of calculations get the exact size of the final transaction, and then change again //Due to the change, the script needs to be unlocked again in the second round //let the fee to be exact in the second round for (let c = 0; c < 2; c++) { txComposer.clearChangeOutput(); const changeOutputIndex = txComposer.appendChangeOutput(changeAddress, this.feeb, unsignSigPlaceHolderSize); let