UNPKG

sensible-sdk

Version:

Sensible-SDK

954 lines (953 loc) 131 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.SensibleNFT = exports.defaultSignerConfigs = exports.Prevouts = exports.sighashType = void 0; const scryptlib_1 = require("scryptlib"); const BN = require("../bn.js"); const bsv = require("../bsv"); const DustCalculator_1 = require("../common/DustCalculator"); const error_1 = require("../common/error"); const protoheader_1 = require("../common/protoheader"); const SatotxSigner_1 = require("../common/SatotxSigner"); const satotxSignerUtil_1 = require("../common/satotxSignerUtil"); const SizeTransaction_1 = require("../common/SizeTransaction"); const TokenUtil = require("../common/tokenUtil"); const Utils = require("../common/utils"); const utils_1 = require("../common/utils"); const sensible_api_1 = require("../sensible-api"); const tx_composer_1 = require("../tx-composer"); const nft_1 = require("./contract-factory/nft"); const nftGenesis_1 = require("./contract-factory/nftGenesis"); const nftSell_1 = require("./contract-factory/nftSell"); const nftUnlockContractCheck_1 = require("./contract-factory/nftUnlockContractCheck"); const nftProto = require("./contract-proto/nft.proto"); const nft_proto_1 = require("./contract-proto/nft.proto"); const nftSellProto = require("./contract-proto/nftSell.proto"); const contractUtil_1 = require("./contractUtil"); const Signature = bsv.crypto.Signature; exports.sighashType = Signature.SIGHASH_ALL | Signature.SIGHASH_FORKID; const dummyNetwork = "mainnet"; const dummyWif = "L5k7xi4diSR8aWoGKojSNTnc3YMEXEoNpJEaGzqWimdKry6CFrzz"; const dummyPrivateKey = bsv.PrivateKey.fromWIF(dummyWif); const dummyAddress = dummyPrivateKey.toAddress(dummyNetwork); const dummyPk = dummyPrivateKey.toPublicKey(); const dummyTxId = "c776133a77886693ba2484fe12d6bdfb8f8bcb7a237e4a8a6d0f69c7d1879a08"; const dummyUtxo = { txId: dummyTxId, outputIndex: 0, satoshis: 1000000000, wif: dummyWif, }; class Prevouts { constructor() { this._buf = Buffer.alloc(0); } addVout(txId, outputIndex) { const txidBuf = TokenUtil.getTxIdBuf(txId); const indexBuf = TokenUtil.getUInt32Buf(outputIndex); this._buf = Buffer.concat([this._buf, txidBuf, indexBuf]); } toHex() { return this._buf.toString("hex"); } } exports.Prevouts = Prevouts; exports.defaultSignerConfigs = [ { satotxApiPrefix: "https://s1.satoplay.cn,https://s1.satoplay.com", satotxPubKey: "2c8c0117aa5edba9a4539e783b6a1bdbc1ad88ad5b57f3d9c5cba55001c45e1fedb877ebc7d49d1cfa8aa938ccb303c3a37732eb0296fee4a6642b0ff1976817b603404f64c41ec098f8cd908caf64b4a3aada220ff61e252ef6d775079b69451367eda8fdb37bc55c8bfd69610e1f31b9d421ff44e3a0cfa7b11f334374827256a0b91ce80c45ffb798798e7bd6b110134e1a3c3fa89855a19829aab3922f55da92000495737e99e0094e6c4dbcc4e8d8de5459355c21ff055d039a202076e4ca263b745a885ef292eec0b5a5255e6ecc45534897d9572c3ebe97d36626c7b1e775159e00b17d03bc6d127260e13a252afd89bab72e8daf893075f18c1840cb394f18a9817913a9462c6ffc8951bee50a05f38da4c9090a4d6868cb8c955e5efb4f3be4e7cf0be1c399d78a6f6dd26a0af8492dca67843c6da9915bae571aa9f4696418ab1520dd50dd05f5c0c7a51d2843bd4d9b6b3b79910e98f3d98099fd86d71b2fac290e32bdacb31943a8384a7668c32a66be127b74390b4b0dec6455", }, { satotxApiPrefix: "https://satotx.showpay.top", satotxPubKey: "5b94858991d384c61ffd97174e895fcd4f62e4fea618916dc095fe4c149bbdf1188c9b33bc15cbe963a63b2522e70b80a5b722ac0e6180407917403755df4de27d69cc115c683a99face8c823cbccf73c7f0d546f1300b9ee2e96aea85542527f33b649f1885caebe19cf75d9a645807f03565c65bd4c99c8f6bb000644cfb56969eac3e9331c254b08aa279ceb64c47ef66be3f071e28b3a5a21e48cdfc3335d8b52e80a09a104a791ace6a2c1b4da88c52f9cc28c54a324e126ec91a988c1fe4e21afc8a84d0e876e01502386f74e7fc24fc32aa249075dd222361aea119d4824db2a797d58886e93bdd60556e504bb190b76a451a4e7b0431973c0410e71e808d0962415503931bbde3dfce5186b371c5bf729861f239ef626b7217d071dfd62bac877a847f2ac2dca07597a0bb9dc1969bed40606c025c4ff7b53a4a6bd921642199c16ede8165ed28da161739fa8d33f9f483212759498c1219d246092d14c9ae63808f58f03c8ca746904ba51fa326d793cea80cda411c85d35894bdb5", }, { satotxApiPrefix: "https://satotx.volt.id", satotxPubKey: "3a62ce90c189ae322150cfc68cd00739cd681babf46a9b27793413ad780ea7c4ef22afd0042bc3711588587c2b8a953ced78496cb95579b1272b8979183ea3c66d204c8eeffebfa115c596c0c561f3569fe6d6e8e06d7e82192a24a84b739838ac846db8594a565679d617695f184eb85a3902a036eb8e82f95b83acc207f0deeac87291539865765899d97cfe41169c555480372195729269ae30b6c39324a6731d6f4e46da5ba1789c6e9bd14b16426d35fd4449eecd177e2834e87fb65d9d469176ffe0c12097fcc7e2393dbaa504631487a3ad725235b4d25fe3d09c2460f8a6c0bf4defc1ffe65d5fa28e85fae11eace2a66e48a0ae2ed6bcfb4bb94296717a4a5b1b3fa9b0fb3c165e517b9b69fa8aaca7fdc7351a0ac14d110258f442f423a780bebd87ac10173ca00ee4e9f56ced0510e7f53ed41411b91286f288438c361d2a15868d1c84d6a73510ef23eee9312ae2a7162c1fcd5438788236c0571ee822c326ebd123b8a6636e7b192db2911725a20da027bfaa79c33f58174285", }, { satotxApiPrefix: "https://satotx.metasv.com", satotxPubKey: "19d9193ee2e95d09445d28408e8a3da730b2d557cd8d39a7ae4ebbfbceb17ed5d745623529ad33d043511f3e205c1f92b6322833424d19823c3b611b3adabb74e1006e0e93a8f1e0b97ab801c6060a4c060f775998d9f003568ab4ea7633a0395eb761c36106e229394f2c271b8522a44a5ae759254f5d22927923ba85b3729460ecccca07a5556299aa7f2518814c74a2a4d48b48013d609002631f2d93c906d07077ef58d473e3d971362d1129c1ab9b8f9b1365519f0c023c1cadad5ab57240d19e256e08022fd0708951ff90a8af0655aff806c6382d0a72c13f1e52b88222d7dfc6357179b06ffcf937f9da3b0419908aa589a731e26bbaba2fa0b754bf722e338c5627b11dc24aadc4d83c35851c034936cf0df18167e856a5f0a7121d23cd48b3f8a420869a37bd1362905d7f76ff18a991f75a0f9d1bcfc18416d76691cc357cbdcc8cc0df9dbd9318a40e08adb2fb4e78b3c47bdf07eeed4f3f4e0f7e81e37460a09b857e0194c72ec03bb564b5b409d8a1b84c153186ecbb4cfdfd", }, { satotxApiPrefix: "https://satotx.tswap.io", satotxPubKey: "a36531727b324b34baef257d223b8ba97bac06d6b631cccb271101f20ef1de2523a0a3a5367d89d98ff354fe1a07bcfb00425ab252950ce10a90dc9040930cf86a3081f0c68ea05bfd40aab3e8bfaaaf6b5a1e7a2b202892dc9b1c0fe478210799759b31ee04e842106a58d901eb5bc538c1b58b7eb774a382e7ae0d6ed706bb0b12b9b891828da5266dd9f0b381b05ecbce99fcde628360d281800cf8ccf4630b2a0a1a25cf4d103199888984cf61edaa0dad578b80dbce25b3316985a8f846ada9bf9bdb8c930e2a43e69994a9b15ea33fe6ee29fa1a6f251f8d79a5de9f1f24152efddedc01b63e2f2468005ecce7da382a64d7936b22a7cac697e1b0a48419101a802d3be554a9b582a80e5c5d8b998e5eb9684c7aaf09ef286d3d990c71be6e3f3340fdaeb2dac70a0be928b6de6ef79f353c868def3385bccd36aa871eb7c8047d3f10b0a38135cdb3577eaafa512111a7af088e8f77821a27b195c95bf80da3c59fda5ff3dd1d40f60d61c099a608b58b6de4a76146cf7b89444c1055", }, ]; contractUtil_1.ContractUtil.init(); function checkParamUtxoFormat(utxo) { if (utxo) { if (!utxo.txId || !utxo.satoshis || !utxo.wif) { throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, `UtxoFormatError-valid format example :{ txId:'85f583e7a8e8b9cf86e265c2594c1e4eb45db389f6781c3b1ec9aa8e48976caa', satoshis:1000, outputIndex:1, wif:'L3J1A6Xyp7FSg9Vtj3iBKETyVpr6NibxUuLhw3uKpUWoZBLkK1hk' }`); } } } function checkParamSigners(signers) { if (signers.length != 5) { throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, "only support 5 signers"); } let signer = signers[0]; if (Utils.isNull(signer.satotxApiPrefix) || Utils.isNull(signer.satotxPubKey)) { throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, `SignerFormatError-valid format example : signers:[{ satotxApiPrefix: "https://api.satotx.com", satotxPubKey: "25108ec89eb96b99314619eb5b124f11f00307a833cda48f5ab1865a04d4cfa567095ea4dd47cdf5c7568cd8efa77805197a67943fe965b0a558216011c374aa06a7527b20b0ce9471e399fa752e8c8b72a12527768a9fc7092f1a7057c1a1514b59df4d154df0d5994ff3b386a04d819474efbd99fb10681db58b1bd857f6d5", },...]`); } } function checkParamNetwork(network) { if (!["mainnet", "testnet"].includes(network)) { throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, `NetworkFormatError:only support 'mainnet' and 'testnet' but value is ${network}`); } } function checkParamGenesis(genesis) { if (typeof genesis != "string" || genesis.length != 40) { throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, `GenesisFormatError:genesis should be a string with 40 length `); } } function checkParamCodehash(codehash) { if (typeof codehash != "string" || codehash.length != 40) { throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, `CodehashFormatError:codehash should be a string with 40 length `); } // $.checkArgument( // codehash == ContractUtil.tokenCodeHash, // `a valid codehash should be ${ContractUtil.tokenCodeHash}, but the provided is ${codehash} ` // ); } function checkParamSensibleId(sensibleId) { if (typeof sensibleId != "string" || sensibleId.length != 72) { throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, `CodehashFormatError:sensibleId should be a string with 72 length `); } } 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, }; } /** Sensible Non Fungible Token */ class SensibleNFT { /** * * @param signers * @param signerSelecteds (Optional) the indexs of the signers which is decided to verify * @param feeb (Optional) the fee rate. default is 0.5 * @param network (Optional) mainnet/testnet default is mainnet * @param purse (Optional) the private key to offer transacions fee. If not provided, bsv utoxs must be provided in genesis/issue/transfer. * @param debug (Optional) specify if verify the tx when genesis/issue/transfer, default is false * @param apiTarget (Optional) SENSIBLE/METASV, default is SENSIBLE. * @param dustLimitFactor (Optional) specify the output dust rate, default is 0.25 .If the value is equal to 0, the final dust will be at least 1. * @param dustAmount (Optional) specify the output dust. */ constructor({ signers = exports.defaultSignerConfigs, signerSelecteds, feeb = 0.05, network = sensible_api_1.API_NET.MAIN, purse, debug = false, apiTarget = sensible_api_1.API_TARGET.SENSIBLE, apiUrl, mockData, dustLimitFactor = 300, dustAmount, }) { this.signerSelecteds = []; checkParamNetwork(network); if (mockData) { this.signers = mockData.satotxSigners; } else { checkParamSigners(signers); this.signers = signers.map((v) => new SatotxSigner_1.SatotxSigner(v.satotxApiPrefix, v.satotxPubKey)); } this.feeb = feeb; this.network = network; if (mockData) { this.sensibleApi = mockData.sensibleApi; } else { this.sensibleApi = new sensible_api_1.SensibleApi(network, apiTarget, apiUrl); } this.debug = debug; this.dustCalculator = new DustCalculator_1.DustCalculator(dustLimitFactor, dustAmount); if (network == sensible_api_1.API_NET.MAIN) { this.zeroAddress = new bsv.Address("1111111111111111111114oLvT2"); } else { this.zeroAddress = new bsv.Address("mfWxJ45yp2SFn7UciZyNpvDKrzbhyfKrY8"); } if (purse) { const privateKey = bsv.PrivateKey.fromWIF(purse); const address = privateKey.toAddress(this.network); this.purse = { privateKey, address, }; } if (signerSelecteds) { if (signerSelecteds.length < nft_proto_1.SIGNER_VERIFY_NUM) { throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, `the length of signerSeleteds should not less than ${nft_proto_1.SIGNER_VERIFY_NUM}`); } this.signerSelecteds = signerSelecteds; } else { for (let i = 0; i < nft_proto_1.SIGNER_VERIFY_NUM; i++) { this.signerSelecteds.push(i); } } this.signerSelecteds.sort((a, b) => a - b); let rabinPubKeys = this.signers.map((v) => v.satotxPubKey); let rabinPubKeyHashArray = TokenUtil.getRabinPubKeyHashArray(rabinPubKeys); this.rabinPubKeyHashArrayHash = bsv.crypto.Hash.sha256ripemd160(rabinPubKeyHashArray); this.rabinPubKeyHashArray = new scryptlib_1.Bytes((0, scryptlib_1.toHex)(rabinPubKeyHashArray)); this.rabinPubKeyArray = rabinPubKeys.map((v) => new scryptlib_1.Int(v.toString(10))); this.unlockContractCodeHashArray = contractUtil_1.ContractUtil.unlockContractCodeHashArray; } /** * Pick the signer with the best connectivity * @param signerConfigs * @returns */ static selectSigners(signerConfigs = exports.defaultSignerConfigs) { return __awaiter(this, void 0, void 0, function* () { return yield (0, satotxSignerUtil_1.selectSigners)(signerConfigs, nftProto.SIGNER_NUM, nftProto.SIGNER_VERIFY_NUM); }); } /** * set dust. DustAmount has a higher priority than dustLimitFactor * @param dustLimitFactor specify the output dust rate, default is 0.25 .If the value is equal to 0, the final dust will be at least 1. * @param dustAmount specify the output dust */ setDustThreshold({ dustLimitFactor, dustAmount, }) { this.dustCalculator.dustAmount = dustAmount; this.dustCalculator.dustLimitFactor = dustLimitFactor; } getDustThreshold(size) { return this.dustCalculator.getDustThreshold(size); } _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.sensibleApi.getUnspents(this.purse.address.toString()); paramUtxos.forEach((v) => { utxoPrivateKeys.push(this.purse.privateKey); }); } else { paramUtxos.forEach((v) => { if (v.wif) { let privateKey = new bsv.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 bsv.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 }; }); } _pretreatNftUtxoToTransfer(tokenIndex, codehash, genesis, senderPrivateKey, senderPublicKey) { return __awaiter(this, void 0, void 0, function* () { if (senderPrivateKey) { senderPublicKey = senderPrivateKey.toPublicKey(); } let _res = yield this.sensibleApi.getNonFungibleTokenUnspentDetail(codehash, genesis, tokenIndex); let nftUtxo = { txId: _res.txId, outputIndex: _res.outputIndex, nftAddress: new bsv.Address(_res.tokenAddress, this.network), publicKey: senderPublicKey, }; return { nftUtxo, nftUtxoPrivateKey: senderPrivateKey }; }); } _pretreatNftUtxoToIssue({ sensibleId, genesisPublicKey, }) { return __awaiter(this, void 0, void 0, function* () { let genesisContract = nftGenesis_1.NftGenesisFactory.createContract(genesisPublicKey); let { genesisTxId, genesisOutputIndex } = parseSensibleID(sensibleId); let genesisUtxo = yield this.getIssueUtxo(genesisContract.getCodeHash(), genesisTxId, genesisOutputIndex); if (!genesisUtxo) { throw new error_1.CodeError(error_1.ErrCode.EC_FIXED_TOKEN_SUPPLY, "token supply is fixed"); } let txHex = yield this.sensibleApi.getRawTxData(genesisUtxo.txId); const tx = new bsv.Transaction(txHex); let preTxId = tx.inputs[0].prevTxId.toString("hex"); let preOutputIndex = tx.inputs[0].outputIndex; let preTxHex = yield this.sensibleApi.getRawTxData(preTxId); genesisUtxo.satotxInfo = { txId: genesisUtxo.txId, outputIndex: genesisUtxo.outputIndex, txHex, preTxId, preOutputIndex, preTxHex, }; let output = tx.outputs[genesisUtxo.outputIndex]; genesisUtxo.satoshis = output.satoshis; genesisUtxo.lockingScript = output.script; genesisContract.setFormatedDataPartFromLockingScript(genesisUtxo.lockingScript); return { genesisContract, genesisTxId, genesisOutputIndex, genesisUtxo, }; }); } _pretreatNftUtxoToTransferOn(nftUtxo, codehash, genesis) { return __awaiter(this, void 0, void 0, function* () { let txHex = yield this.sensibleApi.getRawTxData(nftUtxo.txId); const tx = new bsv.Transaction(txHex); let tokenScript = tx.outputs[nftUtxo.outputIndex].script; let curDataPartObj = nftProto.parseDataPart(tokenScript.toBuffer()); let input = tx.inputs.find((input) => { let script = new bsv.Script(input.script); if (script.chunks.length > 0) { const lockingScriptBuf = TokenUtil.getLockingScriptFromPreimage(script.chunks[0].buf); if (lockingScriptBuf) { if (nftProto.getQueryGenesis(lockingScriptBuf) == genesis) { return true; } let dataPartObj = nftProto.parseDataPart(lockingScriptBuf); dataPartObj.sensibleID = curDataPartObj.sensibleID; dataPartObj.tokenIndex = BN.Zero; const newScriptBuf = nftProto.updateScript(lockingScriptBuf, dataPartObj); let genesisHash = (0, scryptlib_1.toHex)(bsv.crypto.Hash.sha256ripemd160(newScriptBuf)); if (genesisHash == curDataPartObj.genesisHash) { return true; } } } }); if (!input) throw new error_1.CodeError(error_1.ErrCode.EC_INNER_ERROR, "invalid nftUtxo"); let preTxId = input.prevTxId.toString("hex"); let preOutputIndex = input.outputIndex; let preTxHex = yield this.sensibleApi.getRawTxData(preTxId); const preTx = new bsv.Transaction(preTxHex); nftUtxo.satotxInfo = { txId: nftUtxo.txId, outputIndex: nftUtxo.outputIndex, txHex, preTxId, preOutputIndex, preTxHex, }; nftUtxo.preLockingScript = preTx.outputs[preOutputIndex].script; nftUtxo.lockingScript = tx.outputs[nftUtxo.outputIndex].script; nftUtxo.satoshis = tx.outputs[nftUtxo.outputIndex].satoshis; nftUtxo.preNftAddress = bsv.Address.fromPublicKeyHash(Buffer.from(nftProto.getNftAddress(preTx.outputs[preOutputIndex].script.toBuffer()), "hex"), this.network); return nftUtxo; }); } /** * Create a transaction for genesis * @param genesisWif the private key of the token genesiser * @param totalSupply total supply, 8 bytes unsign int * @param opreturnData (Optional) append an opReturn output * @param utxos (Optional) specify bsv utxos * @param changeAddress (Optional) specify bsv changeAddress * @param noBroadcast (Optional) whether not to broadcast the transaction, the default is false * @returns */ genesis({ genesisWif, totalSupply, opreturnData, utxos, changeAddress, noBroadcast = false, }) { return __awaiter(this, void 0, void 0, function* () { const genesisPrivateKey = new bsv.PrivateKey(genesisWif); const genesisPublicKey = genesisPrivateKey.toPublicKey(); let utxoInfo = yield this._pretreatUtxos(utxos); if (changeAddress) { changeAddress = new bsv.Address(changeAddress, this.network); } else { changeAddress = utxoInfo.utxos[0].address; } totalSupply = new BN(totalSupply.toString()); let { txComposer } = yield this._genesis({ genesisPublicKey, totalSupply, opreturnData, utxos: utxoInfo.utxos, utxoPrivateKeys: utxoInfo.utxoPrivateKeys, changeAddress, }); let { codehash, genesis, sensibleId } = this.getCodehashAndGensisByTx(txComposer.tx); let txHex = txComposer.getRawHex(); if (!noBroadcast) { yield this.sensibleApi.broadcast(txHex); } return { codehash, genesis, sensibleId, tx: txComposer.tx, txid: txComposer.tx.id, txHex, }; }); } /** * create an unsigned transaction for genesis * @param genesisPublicKey the public key of the token genesiser * @param totalSupply total supply, 8 bytes unsign int * @param opreturnData (Optional) append an opReturn output * @param utxos (Optional) specify bsv utxos * @param changeAddress (Optional) specify bsv changeAddress * @returns */ unsignGenesis({ genesisPublicKey, totalSupply, opreturnData, utxos, changeAddress, }) { return __awaiter(this, void 0, void 0, function* () { genesisPublicKey = new bsv.PublicKey(genesisPublicKey); let utxoInfo = yield this._pretreatUtxos(utxos); if (changeAddress) { changeAddress = new bsv.Address(changeAddress, this.network); } else { changeAddress = utxoInfo.utxos[0].address; } totalSupply = new BN(totalSupply.toString()); let { txComposer } = yield this._genesis({ genesisPublicKey, totalSupply, opreturnData, utxos: utxoInfo.utxos, utxoPrivateKeys: utxoInfo.utxoPrivateKeys, changeAddress, }); let tx = txComposer.tx; let sigHashList = txComposer.getSigHashLit(); return { tx, sigHashList }; }); } _genesis({ genesisPublicKey, totalSupply, opreturnData, utxos, utxoPrivateKeys, changeAddress, }) { return __awaiter(this, void 0, void 0, function* () { let genesisContract = nftGenesis_1.NftGenesisFactory.createContract(genesisPublicKey); genesisContract.setFormatedDataPart({ totalSupply, rabinPubKeyHashArrayHash: (0, scryptlib_1.toHex)(this.rabinPubKeyHashArrayHash), }); const txComposer = new tx_composer_1.TxComposer(); const p2pkhInputIndexs = utxos.map((utxo) => { const inputIndex = txComposer.appendP2PKHInput(utxo); txComposer.addSigHashInfo({ inputIndex, address: utxo.address.toString(), sighashType: exports.sighashType, contractType: Utils.CONTRACT_TYPE.P2PKH, }); return inputIndex; }); const genesisOutputIndex = txComposer.appendOutput({ lockingScript: genesisContract.lockingScript, satoshis: this.getDustThreshold(genesisContract.lockingScript.toBuffer().length), }); //If there is opReturn, add it to the second output if (opreturnData) { txComposer.appendOpReturnOutput(opreturnData); } txComposer.appendChangeOutput(changeAddress, this.feeb); if (utxoPrivateKeys && utxoPrivateKeys.length > 0) { p2pkhInputIndexs.forEach((inputIndex) => { let privateKey = utxoPrivateKeys.splice(0, 1)[0]; txComposer.unlockP2PKHInput(privateKey, inputIndex); }); } this._checkTxFeeRate(txComposer); return { txComposer }; }); } /** * Mint a NFT * @param genesis the genesis of NFT. * @param codehash the codehash of NFT. * @param sensibleId the sensibleId of NFT. * @param genesisWif the private key of the NFT genesiser * @param receiverAddress the NFT receiver address * @param metaTxId the txid of meta info outpoint.To describe NFT status, metaId is recommended * @param metaOutputIndex the index of meta info outpoint. * @param opreturnData (Optional) append an opReturn output * @param utxos (Optional) specify bsv utxos * @param changeAddress (Optional) specify bsv changeAddress * @param noBroadcast (Optional) whether not to broadcast the transaction, the default is false * @returns */ issue({ genesis, codehash, sensibleId, genesisWif, receiverAddress, metaTxId, metaOutputIndex, opreturnData, utxos, changeAddress, noBroadcast = false, }) { return __awaiter(this, void 0, void 0, function* () { checkParamGenesis(genesis); checkParamCodehash(codehash); checkParamSensibleId(sensibleId); const genesisPrivateKey = new bsv.PrivateKey(genesisWif); const genesisPublicKey = genesisPrivateKey.toPublicKey(); let utxoInfo = yield this._pretreatUtxos(utxos); if (changeAddress) { changeAddress = new bsv.Address(changeAddress, this.network); } else { changeAddress = utxoInfo.utxos[0].address; } receiverAddress = new bsv.Address(receiverAddress, this.network); let { txComposer, tokenIndex } = yield this._issue({ genesis, codehash, sensibleId, genesisPrivateKey, genesisPublicKey, receiverAddress, metaTxId, metaOutputIndex, opreturnData, utxos: utxoInfo.utxos, utxoPrivateKeys: utxoInfo.utxoPrivateKeys, changeAddress: changeAddress, }); let txHex = txComposer.getRawHex(); if (!noBroadcast) { yield this.sensibleApi.broadcast(txHex); } return { txHex, txid: txComposer.getTxId(), tx: txComposer.getTx(), tokenIndex, }; }); } /** * Create the unsigned transaction for issue NFT, * @param genesis the genesis of NFT. * @param codehash the codehash of NFT. * @param genesisPublicKey the public key of the NFT genesiser * @param receiverAddress the NFT receiver address * @param metaTxId the txid of meta info outpoint.To describe NFT status, metaId is recommended * @param metaOutputIndex the index of meta info outpoint.. * @param opreturnData (Optional) append an opReturn output * @param utxos (Optional) specify bsv utxos * @param changeAddress (Optional) specify bsv changeAddress * @returns */ unsignIssue({ genesis, codehash, sensibleId, genesisPublicKey, receiverAddress, metaTxId, metaOutputIndex, opreturnData, utxos, changeAddress, }) { return __awaiter(this, void 0, void 0, function* () { checkParamGenesis(genesis); checkParamCodehash(codehash); checkParamSensibleId(sensibleId); genesisPublicKey = new bsv.PublicKey(genesisPublicKey); let utxoInfo = yield this._pretreatUtxos(utxos); if (changeAddress) { changeAddress = new bsv.Address(changeAddress, this.network); } else { changeAddress = utxoInfo.utxos[0].address; } receiverAddress = new bsv.Address(receiverAddress, this.network); let { txComposer, tokenIndex } = yield this._issue({ genesis, codehash, sensibleId, genesisPublicKey, receiverAddress, metaTxId, metaOutputIndex, opreturnData, utxos: utxoInfo.utxos, utxoPrivateKeys: utxoInfo.utxoPrivateKeys, changeAddress: changeAddress, }); let tx = txComposer.getTx(); let sigHashList = txComposer.getSigHashLit(); return { tx, sigHashList, tokenIndex }; }); } getIssueUtxo(codehash, genesisTxId, genesisOutputIndex) { return __awaiter(this, void 0, void 0, function* () { let unspent; let firstGenesisTxHex = yield this.sensibleApi.getRawTxData(genesisTxId); let firstGenesisTx = new bsv.Transaction(firstGenesisTxHex); let scriptBuffer = firstGenesisTx.outputs[genesisOutputIndex].script.toBuffer(); let originGenesis = nftProto.getQueryGenesis(scriptBuffer); let genesisUtxos = yield this.sensibleApi.getNonFungibleTokenUnspents(codehash, originGenesis, this.zeroAddress.toString()); unspent = genesisUtxos.find((v) => v.txId == genesisTxId && v.outputIndex == genesisOutputIndex); // let spent = await this.sensibleApi.getOutpointSpent( // genesisTxId, // genesisOutputIndex // ); // if (!spent) { // return { // txId: genesisTxId, // outputIndex: genesisOutputIndex, // }; // } if (!unspent) { let _dataPartObj = nftProto.parseDataPart(scriptBuffer); _dataPartObj.sensibleID = { txid: genesisTxId, index: genesisOutputIndex, }; let newScriptBuf = nftProto.updateScript(scriptBuffer, _dataPartObj); let issueGenesis = nftProto.getQueryGenesis(newScriptBuf); let issueUtxos = yield this.sensibleApi.getNonFungibleTokenUnspents(codehash, issueGenesis, this.zeroAddress.toString()); if (issueUtxos.length > 0) { unspent = issueUtxos[0]; } } if (unspent) { return { txId: unspent.txId, outputIndex: unspent.outputIndex, }; } }); } prepareNftUtxo2(nftUtxo) { return __awaiter(this, void 0, void 0, function* () { let txHex = yield this.sensibleApi.getRawTxData(nftUtxo.txId); const tx = new bsv.Transaction(txHex); let preTxId = tx.inputs[0].prevTxId.toString("hex"); let preOutputIndex = tx.inputs[0].outputIndex; let preTxHex = yield this.sensibleApi.getRawTxData(preTxId); nftUtxo.satotxInfo = { txId: nftUtxo.txId, outputIndex: nftUtxo.outputIndex, txHex, preTxId, preOutputIndex, preTxHex, }; let output = tx.outputs[nftUtxo.outputIndex]; nftUtxo.satoshis = output.satoshis; nftUtxo.lockingScript = output.script; }); } _issue({ genesis, codehash, sensibleId, genesisPrivateKey, genesisPublicKey, receiverAddress, metaTxId, metaOutputIndex, opreturnData, utxos, utxoPrivateKeys, changeAddress, }) { return __awaiter(this, void 0, void 0, function* () { let { genesisContract, genesisTxId, genesisOutputIndex, genesisUtxo } = yield this._pretreatNftUtxoToIssue({ sensibleId, genesisPublicKey }); let balance = utxos.reduce((pre, cur) => pre + cur.satoshis, 0); let estimateSatoshis = yield this._calIssueEstimateFee({ genesisUtxoSatoshis: genesisUtxo.satoshis, opreturnData, 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 originDataPart = genesisContract.getFormatedDataPart(); genesisContract.setFormatedDataPart({ sensibleID: { txid: genesisTxId, index: genesisOutputIndex, }, tokenIndex: BN.Zero, }); let genesisHash = genesisContract.getScriptHash(); genesisContract.setFormatedDataPart(originDataPart); let nftContract = nft_1.NftFactory.createContract(this.unlockContractCodeHashArray, codehash); nftContract.setFormatedDataPart({ metaidOutpoint: { txid: metaTxId, index: metaOutputIndex, }, nftAddress: (0, scryptlib_1.toHex)(receiverAddress.hashBuffer), totalSupply: genesisContract.getFormatedDataPart().totalSupply, tokenIndex: genesisContract.getFormatedDataPart().tokenIndex, genesisHash, rabinPubKeyHashArrayHash: genesisContract.getFormatedDataPart().rabinPubKeyHashArrayHash, sensibleID: { txid: genesisTxId, index: genesisOutputIndex, }, }); if (originDataPart.rabinPubKeyHashArrayHash != (0, scryptlib_1.toHex)(this.rabinPubKeyHashArrayHash)) { throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_SIGNERS, "Invalid signers."); } let { rabinData, rabinPubKeyIndexArray, rabinPubKeyVerifyArray } = yield (0, satotxSignerUtil_1.getRabinData)(this.signers, this.signerSelecteds, genesisContract.isFirstGenesis() ? null : genesisUtxo.satotxInfo); const txComposer = new tx_composer_1.TxComposer(); //The first input is the genesis contract const genesisInputIndex = txComposer.appendInput(genesisUtxo); txComposer.addSigHashInfo({ inputIndex: genesisInputIndex, address: genesisPublicKey.toAddress(this.network).toString(), sighashType: exports.sighashType, contractType: utils_1.CONTRACT_TYPE.BCP01_NFT_GENESIS, }); const p2pkhInputIndexs = utxos.map((utxo) => { const inputIndex = txComposer.appendP2PKHInput(utxo); txComposer.addSigHashInfo({ inputIndex, address: utxo.address.toString(), sighashType: exports.sighashType, contractType: utils_1.CONTRACT_TYPE.P2PKH, }); return inputIndex; }); let genesisContractSatoshis = 0; const genesisDataPartObj = genesisContract.getFormatedDataPart(); if (genesisDataPartObj.tokenIndex.lt(genesisDataPartObj.totalSupply.sub(BN.One))) { genesisDataPartObj.tokenIndex = genesisDataPartObj.tokenIndex.add(BN.One); genesisDataPartObj.sensibleID = nftContract.getFormatedDataPart().sensibleID; let nextGenesisContract = genesisContract.clone(); nextGenesisContract.setFormatedDataPart(genesisDataPartObj); genesisContractSatoshis = this.getDustThreshold(nextGenesisContract.lockingScript.toBuffer().length); txComposer.appendOutput({ lockingScript: nextGenesisContract.lockingScript, satoshis: genesisContractSatoshis, }); } //The following output is the NFT const nftOutputIndex = txComposer.appendOutput({ lockingScript: nftContract.lockingScript, satoshis: this.getDustThreshold(nftContract.lockingScript.toBuffer().length), }); //If there is opReturn, add it to the output let opreturnScriptHex = ""; if (opreturnData) { const opreturnOutputIndex = txComposer.appendOpReturnOutput(opreturnData); opreturnScriptHex = txComposer .getOutput(opreturnOutputIndex) .script.toHex(); } for (let c = 0; c < 2; c++) { txComposer.clearChangeOutput(); const changeOutputIndex = txComposer.appendChangeOutput(changeAddress, this.feeb); let unlockResult = genesisContract.unlock({ txPreimage: txComposer.getInputPreimage(genesisInputIndex), sig: new scryptlib_1.Sig(genesisPrivateKey ? (0, scryptlib_1.toHex)(txComposer.getTxFormatSig(genesisPrivateKey, genesisInputIndex)) : utils_1.PLACE_HOLDER_SIG), rabinMsg: rabinData.rabinMsg, rabinPaddingArray: rabinData.rabinPaddingArray, rabinSigArray: rabinData.rabinSigArray, rabinPubKeyIndexArray, rabinPubKeyVerifyArray, rabinPubKeyHashArray: this.rabinPubKeyHashArray, genesisSatoshis: genesisContractSatoshis, nftScript: new scryptlib_1.Bytes(txComposer.getOutput(nftOutputIndex).script.toHex()), nftSatoshis: txComposer.getOutput(nftOutputIndex).satoshis, changeAddress: new scryptlib_1.Ripemd160((0, scryptlib_1.toHex)(changeAddress.hashBuffer)), changeSatoshis: changeOutputIndex != -1 ? txComposer.getOutput(changeOutputIndex).satoshis : 0, opReturnScript: new scryptlib_1.Bytes(opreturnScriptHex), }); 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()); } if (utxoPrivateKeys && utxoPrivateKeys.length > 0) { p2pkhInputIndexs.forEach((inputIndex) => { let privateKey = utxoPrivateKeys.splice(0, 1)[0]; txComposer.unlockP2PKHInput(privateKey, inputIndex); }); } this._checkTxFeeRate(txComposer); return { txComposer, tokenIndex: nftContract.getFormatedDataPart().tokenIndex.toString(10), }; }); } /** * Create a transaction and broadcast it to transfer a NFT. * @param genesis the genesis of NFT. * @param codehash the codehash of NFT. * @param tokenIndex the tokenIndex of NFT. * @param senderWif the private key of the token sender,can be wif or other format * @param senderPrivateKey the private key of the token sender * @param receiverAddress the NFT receiver address * @param opreturnData (Optional) append an opReturn output * @param utxos (Optional) specify bsv utxos * @param changeAddress (Optional) specify bsv changeAddress * @param noBroadcast (Optional) whether not to broadcast the transaction, the default is false * @returns */ transfer({ genesis, codehash, tokenIndex, senderWif, senderPrivateKey, receiverAddress, opreturnData, utxos, changeAddress, noBroadcast = false, }) { return __awaiter(this, void 0, void 0, function* () { checkParamGenesis(genesis); checkParamCodehash(codehash); let senderPublicKey; if (senderWif) { senderPrivateKey = new bsv.PrivateKey(senderWif); senderPublicKey = senderPrivateKey.publicKey; } else if (senderPrivateKey) { senderPrivateKey = new bsv.PrivateKey(senderPrivateKey); senderPublicKey = senderPrivateKey.publicKey; } else { throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, "senderPrivateKey should be provided!"); } let nftInfo = yield this._pretreatNftUtxoToTransfer(tokenIndex, codehash, genesis, senderPrivateKey, senderPublicKey); let utxoInfo = yield this._pretreatUtxos(utxos); if (changeAddress) { changeAddress = new bsv.Address(changeAddress, this.network); } else { changeAddress = utxoInfo.utxos[0].address; } receiverAddress = new bsv.Address(receiverAddress, this.network); let { txComposer } = yield this._transfer({ genesis, codehash, nftUtxo: nftInfo.nftUtxo, nftPrivateKey: nftInfo.nftUtxoPrivateKey, receiverAddress, opreturnData, utxos: utxoInfo.utxos, utxoPrivateKeys: utxoInfo.utxoPrivateKeys, changeAddress, }); let txHex = txComposer.getRawHex(); if (!noBroadcast) { yield this.sensibleApi.broadcast(txHex); } return { tx: txComposer.tx, txHex, txid: txComposer.tx.id }; }); } /** * Create an unsigned transaction to transfer a NFT. * @param genesis the genesis of NFT. * @param codehash the codehash of NFT. * @param tokenIndex the tokenIndex of NFT. * @param senderPublicKey the public key of the NFT sender * @param receiverAddress the NFT receiver address * @param opreturnData (Optional) append an opReturn output * @param utxos (Optional) specify bsv utxos * @param changeAddress (Optional) specify bsv changeAddress * @returns */ unsignTransfer({ genesis, codehash, tokenIndex, senderPublicKey, receiverAddress, opreturnData, utxos, changeAddress, }) { return __awaiter(this, void 0, void 0, function* () { checkParamGenesis(genesis); checkParamCodehash(codehash); let nftInfo = yield this._pretreatNftUtxoToTransfer(tokenIndex, codehash, genesis, null, senderPublicKey); let utxoInfo = yield this._pretreatUtxos(utxos); if (changeAddress) { changeAddress = new bsv.Address(changeAddress, this.network); } else { changeAddress = utxoInfo.utxos[0].address; } receiverAddress = new bsv.Address(receiverAddress, this.network); let { txComposer } = yield this._transfer({ genesis, codehash, nftUtxo: nftInfo.nftUtxo, receiverAddress, opreturnData, utxos: utxoInfo.utxos, utxoPrivateKeys: utxoInfo.utxoPrivateKeys, changeAddress, }); let tx = txComposer.tx; let sigHashList = txComposer.getSigHashLit(); return { tx, sigHashList }; }); } _transfer({ genesis, codehash, nftUtxo, nftPrivateKey, receiverAddress, opreturnData, utxos, utxoPrivateKeys, changeAddress, }) { return __awaiter(this, void 0, void 0, function* () { nftUtxo = yield this._pretreatNftUtxoToTransferOn(nftUtxo, codehash, genesis); let genesisScript = nftUtxo.preNftAddress.hashBuffer.equals(Buffer.alloc(20, 0)) ? new scryptlib_1.Bytes(nftUtxo.preLockingScript.toHex()) : new scryptlib_1.Bytes(""); let balance = utxos.reduce((pre, cur) => pre + cur.satoshis, 0); let estimateSatoshis = yield this._calTransferEstimateFee({ nftUtxoSatoshis: nftUtxo.satoshis, genesisScript, opreturnData, 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}.`); } //validate signers const nftScriptBuf = nftUtxo.lockingScript.toBuffer(); let dataPartObj = nftProto.parseDataPart(nftScriptBuf); dataPartObj.nftAddress = (0, scryptlib_1.toHex)(receiverAddress.hashBuffer); const lockingScriptBuf = nftProto.updateScript(nftScriptBuf, dataPartObj); if (dataPartObj.rabinPubKeyHashArrayHash != (0, scryptlib_1.toHex)(this.rabinPubKeyHashArrayHash)) { throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_SIGNERS, "Invalid signers."); } let { rabinDatas, rabinPubKeyIndexArray, rabinPubKeyVerifyArray } = yield (0, satotxSignerUtil_1.getRabinDatas)(this.signers, this.signerSelecteds, [ nftUtxo.satotxInfo, ]); const txComposer = new tx_composer_1.TxComposer(); let nftInput = nftUtxo; let prevouts = new Prevouts(); // token contract input const nftInputIndex = txComposer.appendInput(nftInput); prevouts.addVout(nftInput.txId, nftInput.outputIndex); txComposer.addSigHashInfo({ inputIndex: nftInputIndex, address: nftUtxo.nftAddress.toString(), sighashType: exports.sighashType, contractType: utils_1.CONTRACT_TYPE.BCP01_NFT, }); 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; }); //tx addOutput nft const nftOutputIndex = txComposer.appendOutput({ lockingScript: bsv.Script.fromBuffer(lockingScriptBuf), satoshis: this.getDustThreshold(lockingScriptBuf.length), }); //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); const nftContract = nft_1.NftFactory.createContract(this.unlockContractCodeHashArray, codehash); let dataPartObj = nftProto.parseDataPart(nftInput.lockingScript.toBuffer()); nftContract.setFormatedDataPart(dataPartObj); const unlockingContract = nftContract.unlock({ txPreimage: txComposer.getInputPreimage(nftInputIndex), prevouts: new scryptlib_1.Bytes(prevouts.toHex()),