UNPKG

meta-contract

Version:

Meta Contract SDK

1,013 lines 92.4 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 tokenUtil_1 = 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 tokenUnlockContractCheck_1 = require("../mcp02/contract-factory/tokenUnlockContractCheck"); const buffer_1 = require("buffer"); 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' || codehash === 'e205939ad9956673ce7da9fbd40514b30f66dc35' || codehash === 'c9cc7bbd1010b44873959a8b1a2bcedeb62302b7', `a valid codehash should be ${contractUtil_1.ContractUtil.tokenCodeHash}, but the provided is ${codehash} `); } function determineCodehashVersion(codehash) { if (codehash == contractUtil_1.ContractUtil.tokenCodeHash) { return 2; } return 1; } 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_1.Buffer.from(sensibleID, 'hex'); let genesisTxId = sensibleIDBuf.slice(0, 32).reverse().toString('hex'); let genesisOutputIndex = sensibleIDBuf.readUIntLE(32, 4); return { genesisTxId, genesisOutputIndex, }; } class FtManager { get api() { return this._api; } get sensibleApi() { return this._api; } constructor({ network = __1.API_NET.MAIN, apiTarget = __1.API_TARGET.CYBER3, 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(constants_1.BURN_ADDRESS, network); 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 = debug; } /** * 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({ version = 2, tokenName, tokenSymbol, decimalNum, utxos: utxosInput, changeAddress, opreturnData, genesisWif, noBroadcast = false, }) { return __awaiter(this, void 0, void 0, function* () { // validate params $.checkArgument(_.isString(tokenName) && buffer_1.Buffer.from(tokenName).length <= 40, `tokenName should be a string and not be larger than 40 bytes`); $.checkArgument(_.isString(tokenSymbol) && buffer_1.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'); $.checkArgument(_.isNumber(version) && version >= 1 && version <= 2, 'version should be a number and must be between 1 and 2'); 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)({ version, 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({ version = 2, sensibleId, genesisWif, receiverAddress, tokenAmount, allowIncreaseMints = true, utxos, changeAddress, opreturnData, noBroadcast = false, }) { return __awaiter(this, void 0, void 0, function* () { $.checkArgument(sensibleId, 'sensibleId is required'); $.checkArgument(genesisWif, 'genesisWif is required'); $.checkArgument(receiverAddress, 'receiverAddress is required'); $.checkArgument(tokenAmount, 'tokenAmount is required'); const utxoInfo = yield this._pretreatUtxos(utxos); 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({ version, 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({ version, 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_MVC, `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, version); 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, }); }); } totalMerge({ codehash, genesis, ownerWif, changeAddress, opreturnData, }) { return __awaiter(this, void 0, void 0, function* () { $.checkArgument(ownerWif, 'ownerWif is required'); const ownerPublicKey = mvc.PrivateKey.fromWIF(ownerWif).toPublicKey(); const txids = []; while (true) { const ftUtxos = yield this.api.getFungibleTokenUnspents(codehash, genesis, ownerPublicKey.toAddress(this.network).toString(), 20); if (ftUtxos.length <= 1) { break; } const paramFtUtxos = ftUtxos.map((v) => { return { txId: v.txId, outputIndex: v.outputIndex, tokenAddress: v.tokenAddress, tokenAmount: v.tokenAmount, wif: ownerWif, }; }); const { txid } = yield this.transfer({ codehash, genesis, senderWif: ownerWif, ftUtxos: paramFtUtxos, changeAddress, isMerge: true, receivers: [], opreturnData, }); txids.push(txid); // sleep 1s yield new Promise((resolve) => setTimeout(resolve, 1000)); } return { txids }; }); } _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_MVC, 'Insufficient balance.'); return { utxos, utxoPrivateKeys }; }); } /** * Estimate the cost of genesis * @param opreturnData * @param utxoMaxCount Maximum number of MVC 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_MVC, `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); const version = determineCodehashVersion(codehash); 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; } let { txComposer, transferCheckTxComposer } = yield this._transfer({ version, 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(); if (!noBroadcast) { yield this.api.broadcast(routeCheckTxHex); yield this.api.broadcast(txHex); } return { tx: txComposer.getTx(), txHex, routeCheckTx: transferCheckTxComposer.getTx(), routeCheckTxHex, txid: txComposer.getTxId(), }; }); } /** * burn token * @param codehash token codehash * @param genesis token genesis * @param ftUtxos ft utxos to burn, must be sent to zero address * @param utxos fee provider utxos * @param utxoPrivateKey fee provider utxo private key * @param changeAddress satoshi change address * @param opreturnData opreturn data * @param noBroadcast if true, will not broadcast tx */ burn({ codehash, genesis, ftUtxos, utxos, utxoPrivateKey, changeAddress, opreturnData, noBroadcast = false, }) { return __awaiter(this, void 0, void 0, function* () { checkParamGenesis(genesis); checkParamCodehash(codehash); const version = determineCodehashVersion(codehash); let utxoInfo = yield this._pretreatUtxos(utxos); if (changeAddress) { changeAddress = new mvc.Address(changeAddress, this.network); } else { changeAddress = utxoInfo.utxos[0].address; } let ftUtxoInfo = yield this._pretreatFtUtxos(ftUtxos, codehash, genesis); let { unlockCheckTxComposer, txComposer } = yield this._burn({ version, genesis, ftUtxos: ftUtxoInfo.ftUtxos, utxos: utxoInfo.utxos, utxoPrivateKey: utxoInfo.utxoPrivateKeys[0], changeAddress, opreturnData, }); let routeCheckTxHex = unlockCheckTxComposer.getRawHex(); let txHex = txComposer.getRawHex(); if (!noBroadcast) { yield this.api.broadcast(routeCheckTxHex); yield this.api.broadcast(txHex); } return { tx: txComposer.getTx(), txHex, routeCheckTx: unlockCheckTxComposer.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 }; }); } /** * prepare transfer tokens, decide which transfer pattern to use, preprocess ft utxos(fetch previous transactions for tx building) * @param codehash codehash of token * @param genesis genesis of token * @param receivers token receivers, will be ignored if isMerge is true * @param ftUtxos input ft utxos * @param ftChangeAddress change address of ft * @param isMerge merge utxos, if true, all the token will be merged into one utxo and send to the change address * @param minUtxoSet if true, will use minimum utxo set as possible * @private */ _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, 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.'); } return { tokenInputArray, tokenOutputArray, tokenTransferType, }; }); } /** * prepare burn tokens,preprocess ft utxos(fetch previous transactions for tx building) * @param codehash codehash of token * @param genesis genesis of token * @param ftUtxos input ft utxos * @private */ _prepareBurnTokens({ genesis, ftUtxos }) { return __awaiter(this, void 0, void 0, function* () { 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); } ftUtxos = _ftUtxos; yield this.perfectFtUtxosInfo(ftUtxos, genesis); let tokenInputArray = ftUtxos; // burn tx have only one output to receive satoshi let tokenUnlockType = tokenUnlockContractCheck_1.TokenUnlockContractCheckFactory.getOptimumType(ftUtxos.length, 1); if (tokenUnlockType == tokenUnlockContractCheck_1.TOKEN_UNLOCK_TYPE.UNSUPPORT) { throw new error_1.CodeError(error_1.ErrCode.EC_TOO_MANY_FT_UTXOS, 'Too many token-utxos, should merge them to continue.'); } return { tokenInputArray, tokenUnlockType, }; }); } /** * Fetch previous transactions for each ft utxo * @param ftUtxos ft utxos * @param genesis genesis of token * @private */ perfectFtUtxosInfo(ftUtxos, 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_1.Buffer.from(dataPartObj.tokenAddress, 'hex'), this.network); } v.preLockingScript = preTx.outputs[v.satotxInfo.preOutputIndex].script; // 新增字段 prevTokenTx, v.prevTokenTx = preTx; }); return ftUtxos; }); } /** * composite a token transfer transaction and amount check transaction * @param codehash codehash of the token * @param genesis genesis of the token * @param receivers token receivers * @param ftUtxos input ftUtxos * @param ftPrivateKeys private keys of ftUtxos * @param ftChangeAddress change address of ftUtxos * @param utxos utxos for paying fee * @param utxoPrivateKeys private keys of utxos(fee paying) * @param changeAddress change address of utxos(fee paying) * @param middlePrivateKey * @param middleChangeAddress * @param isMerge whether to merge the token utxos * @param opreturnData opreturn data to be added to the transaction * @param minUtxoSet * @private */ _transfer({ version, codehash, genesis, receivers, ftUtxos, ftPrivateKeys, ftChangeAddress, utxos, utxoPrivateKeys, changeAddress, middlePrivateKey, middleChangeAddress, isMerge, opreturnData, minUtxoSet, }) { return __awaiter(this, void 0, void 0, function* () { // limit the number of fee paying utxos if (utxos.length > 3) { throw new error_1.CodeError(error_1.ErrCode.EC_UTXOS_MORE_THAN_3, 'Mvc utxos should be no more than 3 in the transfer operation, please merge it first '); } if (!middleChangeAddress) { middleChangeAddress = utxos[0].address; middlePrivateKey = utxoPrivateKeys[0]; } // preprocess the ftUtxos, fetch previous tx hex and parse the token amount. decide the token transfer type. let { tokenInputArray, tokenOutputArray, tokenTransferType } = yield this._prepareTransferTokens({ codehash, genesis, receivers, ftUtxos, ftChangeAddress, isMerge, minUtxoSet, }); // calculate the fee let estimateSatoshis = this._calTransferEstimateFee({ p2pkhInputNum: utxos.length, tokenInputArray, tokenOutputArray, tokenTransferType, opreturnData, }); // if fee is not enough, throw error const balance = utxos.reduce((pre, cur) => pre + cur.satoshis, 0); if (balance < estimateSatoshis) { throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_MVC, `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 transferCheck 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(); // add utxo to provide fee for transfer check transaction const transferCheck_p2pkhInputIndexs = utxos.map((utxo) => { const inputIndex = transferCheckTxComposer.appendP2PKHInput(utxo); transferChe