UNPKG

scrypt-ts

Version:

A toolset for building sCrypt smart contract applications on Bitcoin SV network written in typescript.

1,137 lines (1,136 loc) 89.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; 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.SmartContract = void 0; const fs = __importStar(require("fs")); const os_1 = require("os"); const path_1 = require("path"); require("reflect-metadata"); const scryptlib_1 = require("scryptlib"); const indexerReader_1 = require("./transformation/indexerReader"); const lodash_1 = require("lodash"); const functions_1 = require("./builtins/functions"); const types_1 = require("./builtins/types"); const crypto_1 = require("crypto"); const utils_1 = require("../bsv/utils"); const utils_2 = require("./utils"); const decorators_1 = require("./decorators"); const library_1 = require("./library"); const diffUtils_1 = require("./utils/diffUtils"); const utils_3 = require("./utils"); const error_1 = require("./utils/error"); /** * The main contract class. To write a contract, extend this class as such: * @example * ```ts * class YourSmartContract extends SmartContract { * // your smart contract code here * } * ``` * @category SmartContract */ class SmartContract { /** @ignore */ static compileImpl(filePath) { return __awaiter(this, void 0, void 0, function* () { const tmpDir = fs.mkdtempSync((0, path_1.join)((0, os_1.tmpdir)(), "scrypt-ts-")); const result = yield (0, scryptlib_1.compileContractAsync)(filePath, { sourceMap: true, artifact: true, out: tmpDir }); if (result.errors.length > 0) { throw new Error(`Compilation failed for class \`${this.name}\`. Check the output details at project building time!`); } else { const artifactFileName = (0, path_1.basename)(filePath).replace('.scrypt', '.json'); const artifactFile = fs.readdirSync(tmpDir).filter(fn => fn == artifactFileName)[0]; if (artifactFile) { fs.copyFileSync((0, path_1.join)(tmpDir, artifactFile), (0, path_1.join)((0, path_1.dirname)(filePath), artifactFile)); } } return result; }); } /** * compiling the scrypt source which is the output of transpiling. Calling this function to output the contract artifact file. * only used for testing. * This function should not be called in production environment. * @returns {Artifact} if compiling succeed, otherwise it throws error. */ static compile() { return __awaiter(this, void 0, void 0, function* () { const errors = yield this.getTranspileErrors(); if (Array.isArray(errors)) { if (errors[0]) { throw errors[0]; } } let artifact = this._loadCachedArtifact(); if (artifact) { this.DelegateClazz = (0, scryptlib_1.buildContractClass)(artifact); } else { let filePath = this._getScryptFilePath(); const result = yield this.compileImpl(filePath); this.DelegateClazz = (0, scryptlib_1.buildContractClass)(result); artifact = result.toArtifact(); } return artifact; }); } /** @ignore */ static getTranspileErrors() { return __awaiter(this, void 0, void 0, function* () { if (!(0, utils_3.isInNodeEnv)()) { throw new Error(`the 'getTranspileErrors()' method should only be called in Node.js environment!`); } const transformationResult = this._getTransformationResult(); if (!transformationResult.success) { // do not try to run compile if has errors during transpiling return transformationResult.errors; } return []; }); } /** * This function is usually called on the frontend. * The contract class needs to call this function before instantiating. * @param artifactFile a merged contract artifact object, or its file path */ static loadArtifact(artifactFile = undefined) { let artifact = undefined; if (typeof artifactFile === 'undefined') { artifact = this._loadCachedArtifact(); } else if (typeof artifactFile === 'string') { artifact = this._loadArtifact(artifactFile); } else { artifact = artifactFile; } if (artifact === undefined) { throw new Error(`Cannot find the artifact file for contract \`${this.name}\`, run \`npx scrypt-cli@latest compile\` to generate it.`); } this.DelegateClazz = (0, scryptlib_1.buildContractClass)(artifact); } /** * * The contract class needs to call this function before instantiating. * @param artifact a merged contract artifact object */ static getArtifact() { if (!this.DelegateClazz) { throw new Error(`No artifact found, Please compile the contract first.`); } return this.DelegateClazz.artifact; } /** @ignore */ static _getTransformationResult() { // load from file const scryptFilePath = this._getScryptFilePath(); const transResultFilePath = (0, utils_2.alterFileExt)(scryptFilePath, "transformer.json"); //scryptFilePath.replace(/\.scrypt$/, ".transformer.json"); return JSON.parse(fs.readFileSync(transResultFilePath).toString()); } /** @ignore */ static _getScryptFilePath() { const scryptFilePath = Reflect.getMetadata("scrypt:filepath", this); // just return if meta data exists if (scryptFilePath) return scryptFilePath; // find indexer file of the contract let indexFile = (0, path_1.join)('.', indexerReader_1.INDEX_FILE_NAME); if (!fs.existsSync(indexFile)) { throw new Error(`Cannot find \`scrypt.index.json\` file, run \`npx scrypt-cli@latest compile\` to generate it.`); } // find scrypt file path in the indexer let indexer = new indexerReader_1.IndexerReader(indexFile); let filePath = indexer.getFullPath(this.name); if (!fs.existsSync(filePath)) { throw new Error(`Cannot find the bundled scrypt file for contract \`${this.name}\`, run \`npx scrypt-cli@latest compile\` to generate it.`); } // set meta data Reflect.defineMetadata("scrypt:filepath", filePath, this); return filePath; } /** @ignore */ static _loadCachedArtifact() { const scryptFile = this._getScryptFilePath(); const artifactFile = (0, utils_2.alterFileExt)(scryptFile, 'json'); return this._loadArtifact(artifactFile); } /** @ignore */ static _loadArtifact(artifactFile) { if (!fs.existsSync(artifactFile)) { return undefined; } return JSON.parse(fs.readFileSync(artifactFile, 'utf8').toString()); } /** @ignore */ getDelegateClazz() { var _a; return (_a = Object.getOwnPropertyDescriptor(this.constructor, 'DelegateClazz')) === null || _a === void 0 ? void 0 : _a.value; } /** @ignore */ static getDelegateClazz() { return this.DelegateClazz; } getDelegateInstance() { return this.delegateInstance; } setDelegateInstance(delegateInstance) { this.delegateInstance = delegateInstance; } constructor(...args) { /** @ignore */ this.enableUpdateEMC = false; // a flag indicateing whether can update `this.entryMethodCall` /** @ignore */ this._txBuilders = new Map(); const baseClass = Object.getPrototypeOf(this.constructor).name; /** * When a derived subclass is instantiated, the `super()` call may lack parameters required to initialize delegateInstance. * In this case, you need to ensure that `super()` does not throw an exception. * constructor(x: bigint, y: bigint) { * super(1n*3n); // super(1n); * this.setConstructor(...arguments) * this.y = y; * } */ if (baseClass === 'SmartContract') { this._initDelegateInstance(...args); } else { try { this._initDelegateInstance(...args); } catch (error) { } } } _initDelegateInstance(...args) { const DelegateClazz = this.getDelegateClazz(); if (!DelegateClazz) { (0, error_1.throwUnInitializing)(this.constructor.name); } const args_ = args.map((arg, index) => { if (arg instanceof library_1.SmartContractLib) { return arg.getArgs(); } if (arg instanceof types_1.HashedMap || arg instanceof types_1.HashedSet) { const ctorAbi = DelegateClazz.abi.find(abi => abi.type === scryptlib_1.ABIEntityType.CONSTRUCTOR); const type = ctorAbi.params[index].type; arg.attachTo(type, DelegateClazz); return arg; } return arg; }); this.delegateInstance = new DelegateClazz(...args_); this.delegateInstance.isGenesis = false; } /** * Only inherited classes can call this function. * Direct subclasses of `SmartContract` do not need to call this function. * @param args constructor parameters of inherited classes * @onchain */ init(...args) { this._initDelegateInstance(...args); } /** * Execute the contract * @ignore * @deprecated */ verify(entryMethodInvoking) { // Reset OP_CODESEPARATOR counter. this._csNum = 0; if (!(0, utils_3.isInNodeEnv)()) { console.error(`the 'verify' method should only be called in Node.js environment!`); return undefined; } let scryptFile = Object.getPrototypeOf(this).constructor._getScryptFilePath(); const sourceMapFile = scryptFile.replace(/\.scrypt$/, '.scrypt.map'); if (!fs.existsSync(sourceMapFile)) { throw new Error(`cannot find the bundled sourcemap file for \`${typeof this}\` at ${sourceMapFile}`); } let sourceMap = JSON.parse(fs.readFileSync(sourceMapFile).toString()); let txContext = {}; if (this.to) { txContext.tx = this.to.tx; let inputIndex = this.to.inputIndex; txContext.inputIndex = inputIndex; txContext.inputSatoshis = this.to.tx.inputs[inputIndex].output.satoshis; } try { const verifyEntryCall = ((ec) => { const result = ec.verify(txContext); if (!result.success && result.error) { const matches = /\[(.+?)\]\((.+?)#(\d+)\)/.exec(result.error); result.error.substring(0, matches.index); const line = parseInt(matches[3]); const tsLine = sourceMap[line - 1][0][2] + 1; result.error = `[Go to Source](file://${scryptFile}:${tsLine})`; } return result; }).bind(this); let entryCall = this.buildEntryMethodCall(entryMethodInvoking); if (entryCall instanceof Promise) { return entryCall.then(v => verifyEntryCall(v)); } else { return verifyEntryCall(entryCall); } } catch (error) { throw error; // make error throwing from `verify` } } /** * get unlocking script of the contract * @ignore * @deprecated * @example * ```ts * instance.getUnlockingScript((self) => { * self.to = { tx, inputIndex } * // call self's public method to get the unlocking script. * self.unlock(..args); * }) * ``` */ getUnlockingScript(callPub) { try { let r = this.clone().buildEntryMethodCall(callPub); if (r instanceof Promise) { return r.then(v => v.unlockingScript); } else { return r.unlockingScript; } } catch (error) { throw error; // make error throwing from `getUnlockingScript` } } /** * sync properties values to delegateInstance iff it's not the genesis. * @ignore */ syncStateProps() { if (!this.delegateInstance.isGenesis) { const statePropKeys = Reflect.getMetadata("scrypt:stateProps", this) || []; statePropKeys.forEach(statePropKey => { if (this[statePropKey] instanceof library_1.SmartContractLib) { this.delegateInstance[statePropKey] = this[statePropKey].getState(); } else if (this[statePropKey] instanceof types_1.HashedMap) { this.delegateInstance[statePropKey] = this[statePropKey]; } else if (this[statePropKey] instanceof types_1.HashedSet) { this.delegateInstance[statePropKey] = this[statePropKey]; } else { this.delegateInstance[statePropKey] = this[statePropKey]; } }); } } /** * Returns a lockingScript of contract. */ get lockingScript() { this.syncStateProps(); return this.delegateInstance.lockingScript; } /** * Returns script size of lockingScript. */ get scriptSize() { return this.lockingScript.toBuffer().length; } /** * Returns code part of the lockingScript, in hex format. */ get codePart() { return this.delegateInstance.codePart.toHex(); } /** * Returns sha256 hash of the current locking script, formatted as a LE hex string. */ get scriptHash() { const res = (0, scryptlib_1.sha256)(this.lockingScript.toHex()).match(/.{2}/g); return res.reverse().join(''); } /** * If the compiled contract contains any ASM variable templates (e.g. P2PKH.unlock.pubKeyHash), * replace them with the passed values. * @param {AsmVarValues} asmVarValues type that contains the actual values. * @returns {void} */ setAsmVars(asmVarValues) { this.delegateInstance.replaceAsmVars(asmVarValues); } /** * Returns set ASM variable values. */ get asmArgs() { return this.delegateInstance.asmArgs; } /** * Deep clone the contract instance. * @ignore * @param opt properties that only references are copied, but not deep clone their values. * @returns a cloned contract instance */ clone(opt) { this.syncStateProps(); // refCloneProps are properties that only references are copied, but not deep clone their values. const refClonePropNames = ['from', 'to', '_signer', '_provider'].concat((opt === null || opt === void 0 ? void 0 : opt.refCloneProps) || []); const refClonePropValues = refClonePropNames.map(pn => this[pn]); // shadow property references on this before cloning refClonePropNames.forEach(pn => this[pn] = undefined); const obj = (0, lodash_1.cloneDeep)(this); // copy property references to the object refClonePropNames.forEach((pn, idx) => obj[pn] = refClonePropValues[idx]); // recover property references on this refClonePropNames.forEach((pn, idx) => this[pn] = refClonePropValues[idx]); return obj; } /** * * @param opt properties that only references are copied, but not deep clone their values. * @returns a cloned contract instance with `this.from = undefined` and `this.to = undefined` */ next(opt) { const cloned = this.clone(opt); cloned.from = undefined; cloned.to = undefined; cloned.delegateInstance.isGenesis = false; cloned.prependNOPScript(null); return cloned; } /** * Mark the contract as genesis contracts */ markAsGenesis() { this.delegateInstance.isGenesis = true; return this; } /** * A built-in function to create an output containing the new state. It takes an input: the number of satoshis in the output. * @onchain * @param amount the number of satoshis in the output * @returns an output containing the new state */ buildStateOutput(amount) { let outputScript = this.getStateScript(); return functions_1.Utils.buildOutput(outputScript, amount); } /** * A built-in function to create an [change output]{@link https://wiki.bitcoinsv.io/index.php/Change}. * @onchain * @returns */ buildChangeOutput() { if (this.changeAmount > BigInt(0)) { const changeScript = functions_1.Utils.buildPublicKeyHashScript(this.changeAddress); return functions_1.Utils.buildOutput(changeScript, this.changeAmount); } return (0, types_1.toByteString)(""); } /** * A built-in function to create a locking script containing the new state. * @onchain * @returns a locking script that containing the new state */ getStateScript() { let sBuf = functions_1.VarIntWriter.writeBool(false); this.delegateInstance.stateProps .forEach(p => { let value = this[p.name]; if (value instanceof types_1.HashedMap || value instanceof types_1.HashedSet) { value = { _data: value.data() }; } (0, scryptlib_1.flatternArg)(Object.assign({}, p, { value: value }), this.delegateInstance.resolver, { state: true, ignoreValue: false }).forEach(p => { if (['bytes', 'PubKey', 'Ripemd160', 'Sig', 'Sha1', 'SigHashPreimage', 'Sha256', 'SigHashType', 'OpCodeType'].includes(p.type)) { sBuf += functions_1.VarIntWriter.writeBytes(p.value); } else if (p.type === 'int' || p.type === 'PrivKey') { sBuf += functions_1.VarIntWriter.writeInt(p.value); } else if (p.type === 'bool') { sBuf += functions_1.VarIntWriter.writeBool(p.value); } }); }); return this.codePart + functions_1.VarIntWriter.serializeState(sBuf); } /** * A built-in function verifies an ECDSA signature. It takes two inputs from the stack, a public key (on top of the stack) and an ECDSA signature in its DER_CANONISED format concatenated with sighash flags. It outputs true or false on the stack based on whether the signature check passes or fails. * @onchain * @category Signature Verification * @see https://wiki.bitcoinsv.io/index.php/Opcodes_used_in_Bitcoin_Script */ checkSig(signature, publickey, errorMsg = "signature check failed") { if (!this.checkSignatureEncoding(signature) || !this.checkPubkeyEncoding(publickey)) { return false; } let fSuccess = false; this._assertToExist(); const bufSig = Buffer.from(signature, 'hex'); const bufPubkey = Buffer.from((0, types_1.toByteString)(publickey), 'hex'); try { const sig = scryptlib_1.bsv.crypto.Signature.fromTxFormat(bufSig); const pubkey = scryptlib_1.bsv.PublicKey.fromBuffer(bufPubkey, false); const tx = this.to.tx; const inputIndex = this.to.inputIndex || 0; const inputSatoshis = this.to.tx.inputs[inputIndex].output.satoshis; // Cut script until most recent OP_CS. const subScript = this.lockingScript.subScript(this._csNum - 1); fSuccess = tx.verifySignature(sig, pubkey, inputIndex, subScript, scryptlib_1.bsv.crypto.BN.fromNumber(inputSatoshis), scryptlib_1.DEFAULT_FLAGS); } catch (e) { // invalid sig or pubkey fSuccess = false; } if (!fSuccess && bufSig.length) { // because NULLFAIL rule, always throw if catch a wrong signature // https://github.com/bitcoin/bips/blob/master/bip-0146.mediawiki#nullfail throw new Error(errorMsg); } return fSuccess; } /** * Same as `checkPreimage`, but support customized more settings. * @onchain * @param txPreimage The format of the preimage is [specified]{@link https://github.com/bitcoin-sv/bitcoin-sv/blob/master/doc/abc/replay-protected-sighash.md#digest-algorithm} * @param privKey private Key * @param pubKey public key * @param inverseK inverseK * @param r r * @param rBigEndian must be mininally encoded, to conform to strict DER rule https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#der-encoding * @param sigHashType A SIGHASH flag is used to indicate which part of the transaction is signed by the ECDSA signature. * @returns true if `txPreimage` is the preimage of the current transaction. Otherwise false. */ checkPreimageAdvanced(txPreimage, privKey, pubKey, inverseK, r, rBigEndian, sigHashType) { // hash is big endian let h = (0, functions_1.hash256)((0, types_1.toByteString)(txPreimage)); let sig = functions_1.Tx.sign(functions_1.Tx.fromBEUnsigned(h), privKey, inverseK, r, rBigEndian, sigHashType); return this.checkSig(sig, pubKey); } /** * Same as `checkPreimage`, but support customized sighash type * @onchain * @param txPreimage The format of the preimage is [specified]{@link https://github.com/bitcoin-sv/bitcoin-sv/blob/master/doc/abc/replay-protected-sighash.md#digest-algorithm} * @param sigHashType A SIGHASH flag is used to indicate which part of the transaction is signed by the ECDSA signature. * @returns true if `txPreimage` is the preimage of the current transaction. Otherwise false. */ checkPreimageSigHashType(txPreimage, sigHashType) { return this.checkPreimageAdvanced(txPreimage, functions_1.Tx.privKey, functions_1.Tx.pubKey, functions_1.Tx.invK, functions_1.Tx.r, functions_1.Tx.rBigEndian, sigHashType); } /** * Using the [OP_PUSH_TX]{@link https://medium.com/@xiaohuiliu/op-push-tx-3d3d279174c1} technique, check if `txPreimage` is the preimage of the current transaction. * @onchain * @param txPreimage The format of the preimage is [specified]{@link https://github.com/bitcoin-sv/bitcoin-sv/blob/master/doc/abc/replay-protected-sighash.md#digest-algorithm} * @returns true if `txPreimage` is the preimage of the current transaction. Otherwise false. */ checkPreimage(txPreimage) { return this.checkPreimageAdvanced(txPreimage, functions_1.Tx.privKey, functions_1.Tx.pubKey, functions_1.Tx.invK, functions_1.Tx.r, functions_1.Tx.rBigEndian, functions_1.SigHash.ALL); } /** * Insert and OP_CODESEPARATOR at this point of the functions logic. * More detail about [OP_CODESEPARATOR]{@link https://wiki.bitcoinsv.io/index.php/OP_CODESEPARATOR} */ insertCodeSeparator() { // Call transpiled to "***" in native sCrypt. See transpiler.ts. this._csNum++; return; } /** * Compares the first signature against each public key until it finds an ECDSA match. Starting with the subsequent public key, it compares the second signature against each remaining public key until it finds an ECDSA match. The process is repeated until all signatures have been checked or not enough public keys remain to produce a successful result. All signatures need to match a public key. Because public keys are not checked again if they fail any signature comparison, signatures must be placed in the scriptSig using the same order as their corresponding public keys were placed in the scriptPubKey or redeemScript. If all signatures are valid, 1 is returned, 0 otherwise. Due to a bug, one extra unused value is removed from the stack. * @onchain * @category Signature Verification * @see https://wiki.bitcoinsv.io/index.php/Opcodes_used_in_Bitcoin_Script */ checkMultiSig(signatures, publickeys) { for (let i = 0; i < signatures.length; i++) { if (!this.checkSignatureEncoding(signatures[i])) { return false; } } for (let i = 0; i < publickeys.length; i++) { if (!this.checkPubkeyEncoding(publickeys[i])) { return false; } } this._assertToExist(); const tx = this.to.tx; const inputIndex = this.to.inputIndex || 0; const inputSatoshis = this.to.tx.inputs[inputIndex].output.satoshis; let pubKeysVisited = new Set(); for (let i = 0; i < signatures.length; i++) { const sig = scryptlib_1.bsv.crypto.Signature.fromTxFormat(Buffer.from((0, types_1.toByteString)(signatures[i]), 'hex')); let noPubKeyMatch = true; for (let j = 0; j < publickeys.length; j++) { if (pubKeysVisited.has(j)) { continue; } pubKeysVisited.add(j); const pubkey = scryptlib_1.bsv.PublicKey.fromBuffer(Buffer.from((0, types_1.toByteString)(publickeys[j]), 'hex'), false); // Cut script until most recent OP_CS. const subScript = this.lockingScript.subScript(this._csNum - 1); try { let success = tx.verifySignature(sig, pubkey, inputIndex, subScript, scryptlib_1.bsv.crypto.BN.fromNumber(inputSatoshis), scryptlib_1.DEFAULT_FLAGS); if (success) { noPubKeyMatch = false; break; } } catch (e) { continue; } } if (noPubKeyMatch) { return false; } } return true; } /** * Implements a time-based lock on a transaction until a specified `locktime` has been reached. * The lock can be based on either block height or a UNIX timestamp. * * If the `locktime` is below 500,000,000, it's interpreted as a block height. Otherwise, * it's interpreted as a UNIX timestamp. This function checks and ensures that the transaction's * nSequence is less than `UINT_MAX`, and that the provided `locktime` has been reached or passed. * * @param {bigint} locktime - The block height or timestamp until which the transaction should be locked. * @returns If `true` is returned, nlockTime and sequence in `this.ctx` are valid, otherwise they are invalid. * @onchain * @category Time Lock * @see https://docs.scrypt.io/tutorials/timeLock */ timeLock(locktime) { let res = true; // Ensure nSequence is less than UINT_MAX. res = this.ctx.sequence < 0xffffffff; // Check if using block height. if (locktime < 500000000) { // Enforce nLocktime field to also use block height. res = res && this.ctx.locktime < 500000000; } return res && this.ctx.locktime >= locktime; } /** * Get the amount of the change output for `to.tx`. * @onchain * @returns amount in satoshis */ get changeAmount() { this._assertToExist(); return BigInt(this.to.tx.getChangeAmount()); } /** * Get the prevouts for `to.tx`. * @onchain * @returns prevouts in satoshis */ get prevouts() { this._assertToExist(); const sighashType = this.sigTypeOfMethod(this._currentMethod); if (sighashType & scryptlib_1.bsv.crypto.Signature.SIGHASH_ANYONECANPAY) { return (0, types_1.toByteString)(""); } return this.to.tx.prevouts(); } /** * Get the change address of the change output for `to.tx`. * @onchain * @returns the change address of to.tx */ get changeAddress() { this._assertToExist(); // TODO: update bsv index.d.ts later if (!this.to.tx["_changeAddress"]) { throw new Error('No change output found on the transaction'); } return (0, scryptlib_1.PubKeyHash)(this.to.tx["_changeAddress"].toObject().hash); } /** * @ignore * @param publickey * @returns true publickey valid. */ checkPubkeyEncoding(publickey) { if ((scryptlib_1.DEFAULT_FLAGS & scryptlib_1.bsv.Script.Interpreter.SCRIPT_VERIFY_STRICTENC) !== 0 && !scryptlib_1.bsv.PublicKey.isValid((0, types_1.toByteString)(publickey))) { return false; } return true; } /** * @ignore * @param signature * @returns true signature valid. */ checkSignatureEncoding(signature) { var buf = Buffer.from((0, types_1.toByteString)(signature), 'hex'); var sig; // Empty signature. Not strictly DER encoded, but allowed to provide a // compact way to provide an invalid signature for use with CHECK(MULTI)SIG if (buf.length === 0) { return true; } if ((scryptlib_1.DEFAULT_FLAGS & (scryptlib_1.bsv.Script.Interpreter.SCRIPT_VERIFY_DERSIG | scryptlib_1.bsv.Script.Interpreter.SCRIPT_VERIFY_LOW_S | scryptlib_1.bsv.Script.Interpreter.SCRIPT_VERIFY_STRICTENC)) !== 0 && !scryptlib_1.bsv.crypto.Signature.isTxDER(buf)) { return false; } else if ((scryptlib_1.DEFAULT_FLAGS & scryptlib_1.bsv.Script.Interpreter.SCRIPT_VERIFY_LOW_S) !== 0) { sig = scryptlib_1.bsv.crypto.Signature.fromTxFormat(buf); if (!sig.hasLowS()) { return false; } } else if ((scryptlib_1.DEFAULT_FLAGS & scryptlib_1.bsv.Script.Interpreter.SCRIPT_VERIFY_STRICTENC) !== 0) { sig = scryptlib_1.bsv.crypto.Signature.fromTxFormat(buf); if (!sig.hasDefinedHashtype()) { return false; } if (!(scryptlib_1.DEFAULT_FLAGS & scryptlib_1.bsv.Script.Interpreter.SCRIPT_ENABLE_SIGHASH_FORKID) && (sig.nhashtype & scryptlib_1.bsv.crypto.Signature.SIGHASH_FORKID)) { return false; } if ((scryptlib_1.DEFAULT_FLAGS & scryptlib_1.bsv.Script.Interpreter.SCRIPT_ENABLE_SIGHASH_FORKID) && !(sig.nhashtype & scryptlib_1.bsv.crypto.Signature.SIGHASH_FORKID)) { return false; } } return true; } hasPrevouts(methodName) { const abi = this.getDelegateClazz().abi.find(func => func.name === methodName); // only entry (public) method calls will be delegated to the target instance. if (!abi) { throw new Error(`contract ${this.constructor.name} doesnot have method ${methodName}`); } return abi.params.findIndex(param => param.name === "__scrypt_ts_prevouts" && param.type === "bytes") > -1; } /** * call delegateInstance method * @ignore * @param methodName * @param args * @returns */ callDelegatedMethod(methodName, ...args) { const abi = this.getDelegateClazz().abi.find(func => func.name === methodName); // only entry (public) method calls will be delegated to the target instance. if (!abi) return undefined; this._currentMethod = methodName; // explicit params are from the definition of @method const explicitParams = abi.params.slice(0, args.length); // find all params that are hidden from ts @method, which should be auto filled with proper values here. const autoFillParams = abi.params.slice(args.length); let autoFillArgBindings = new Map(); let txPreimage; let prevouts; const accessPathArgCallbacks = new Map(); autoFillParams.forEach((param, idx) => { // auto fill `__scrypt_ts_txPreimage` if (param.name === "__scrypt_ts_txPreimage" && param.type === "SigHashPreimage") { this._assertToExist(); const sighash = this.sigTypeOfMethod(methodName); // this preiamge maybe a cached preimage txPreimage = (0, types_1.SigHashPreimage)(this.to.tx.getPreimage(this.to.inputIndex, sighash)); autoFillArgBindings.set(param.name, txPreimage); } // auto fill `__scrypt_ts_changeAmount` if (param.name === "__scrypt_ts_changeAmount" && param.type === "int") { autoFillArgBindings.set(param.name, this.changeAmount); } // auto fill `__scrypt_ts_changeAddress` if (param.name === "__scrypt_ts_changeAddress" && param.type === "Ripemd160") { autoFillArgBindings.set(param.name, this.changeAddress); } // auto fill `__scrypt_ts_accessPathForProp__${propName}` if (param.name.startsWith("__scrypt_ts_accessPathForProp__") && param.type === "bytes") { const propName = param.name.replace("__scrypt_ts_accessPathForProp__", ""); // traceable object is a HashedMap/HashedSet-typed contract property. let traceableObj = this[propName]; if (!(0, types_1.instanceOfSIATraceable)(traceableObj)) { throw new Error(`Internel error: traceable object \`${propName}\` not found`); } // start tracing SortedItem access on the traceable object traceableObj.startTracing(); // get index of the param in the whole parameters const argIdx = explicitParams.length + idx; // setup a callback function, it will will be evaluated after @method execution accessPathArgCallbacks.set(argIdx, () => { // stop trace traceableObj.stopTracing(); return traceableObj.serializedAccessPath(); }); // set a dummy value as a placeholder for the access path param // if real accesses count > 10, getEstimateFee will fail when auto pay fee autoFillArgBindings.set(param.name, (0, crypto_1.randomBytes)(2 /* bytes per access */ * 30 /* estimated accesses */).toString('hex')); } // auto fill `__scrypt_ts_prev` if (param.name === "__scrypt_ts_prevouts" && param.type === "bytes") { prevouts = this.prevouts; autoFillArgBindings.set(param.name, prevouts); } }); args.push(...autoFillParams.map(p => { const val = autoFillArgBindings.get(p.name); if (val === undefined) { throw new Error(`missing auto-filled value for argument ${p.name}`); } return val; })); return { publicMethodCall: this.encodeMethodCall(methodName, args), txPreimage, prevouts, traceableArgCallbacks: accessPathArgCallbacks, abi }; } /** * Call the public function on the delegateInstance * @ignore * @param methodName * @param args * @returns a `FunctionCall` that contains a unlocking script */ encodeMethodCall(methodName, args) { return this.getDelegateClazz().prototype[methodName].call(this.delegateInstance, ...args); } /** * Set `this.ctx` by a `SigHashPreimage` * @ignore * @param txPreimage */ setCtx(txPreimage) { const outpoint = functions_1.SigHash.outpoint(txPreimage); this._ctx = { version: functions_1.SigHash.nVersion(txPreimage), utxo: { value: functions_1.SigHash.value(txPreimage), script: functions_1.SigHash.scriptCode(txPreimage), outpoint: { txid: outpoint.slice(0, 32 * 2), outputIndex: functions_1.Utils.fromLEUnsigned(outpoint.slice(32 * 2)) } }, hashPrevouts: functions_1.SigHash.hashPrevouts(txPreimage), hashSequence: functions_1.SigHash.hashSequence(txPreimage), sequence: functions_1.SigHash.nSequence(txPreimage), hashOutputs: functions_1.SigHash.hashOutputs(txPreimage), locktime: functions_1.SigHash.nLocktime(txPreimage), sigHashType: functions_1.SigHash.sigHashType(txPreimage), serialize() { return txPreimage; }, }; } /** @ignore */ clearCtx() { this._ctx = undefined; } /** * set the data part of the contract in ASM format * @param dataPart */ setDataPartInASM(dataPart) { this.delegateInstance.setDataPartInASM(dataPart); } /** * set the data part of the contract in hex format * @param dataPart */ setDataPartInHex(dataPart) { this.delegateInstance.setDataPartInHex(dataPart); } get dataPart() { return this.delegateInstance.dataPart; } /** @ignore */ buildEntryMethodCall(callPub) { this.entryMethodCall = undefined; this.enableUpdateEMC = true; const afterCall = (() => { if (!this.entryMethodCall) { throw new Error('a contract public method should be called on the `self` parameter within the `callPub` function'); } const entryCall = this.entryMethodCall; this.entryMethodCall = undefined; this.enableUpdateEMC = false; return entryCall; }).bind(this); // `this.entryMethodCall` should be set properly in call back: `callPub` const callPubRet = callPub(this); if (callPubRet instanceof Promise) { return callPubRet.then(() => afterCall()); } else { return afterCall(); } } /** @ignore */ _assertToExist() { if (!this.to) { throw new Error('`this.to` is undefined, it should be set properly before calling the method'); } } /** @ignore */ _assertFromExist() { if (!this.from) { throw new Error('`this.from` is undefined, it should be set properly before calling the method'); } } /** * connect a signer. * @param signer a signer */ connect(signer) { return __awaiter(this, void 0, void 0, function* () { const isAuthenticated = yield signer.isAuthenticated(); if (!isAuthenticated) { throw new Error("the signer is unauthenticated"); } this._signer = signer; this._provider = signer.provider; }); } /** * set a provider. * @param provider a provider */ setProvider(provider) { this._provider = provider; } /** * Get the connected [signer]{@link https://docs.scrypt.io/how-to-test-a-contract#signer} */ get signer() { if (!this._signer) { throw new Error('No signer found!'); } return this._signer; } /** * Get the connected [provider]{@link https://docs.scrypt.io/how-to-test-a-contract#provider} */ get provider() { if (!this._provider) { throw new Error('No provider found!'); } return this._provider; } /** * creates a tx to deploy the contract. Users override it to cutomize a deployment tx as below. * @example * ```ts * override async buildDeployTransaction(utxos: UTXO[], amount: number, changeAddress?: bsv.Address | string): Promise<bsv.Transaction> { * const deployTx = new bsv.Transaction() * // add p2pkh inputs for paying tx fees * .from(utxos) * // add contract output * .addOutput(new bsv.Transaction.Output({ * script: this.lockingScript, * satoshis: amount, * })) * // add the change output if passing `changeAddress` * if (changeAddress) { * deployTx.change(changeAddress); * if (this._provider) { * deployTx.feePerKb(await this.provider.getFeePerKb()); * } * } * * return deployTx; * } * ``` * @param utxos represents one or more P2PKH inputs for paying transaction fees. * @param amount the balance of contract output * @param changeAddress a change address * @returns */ buildDeployTransaction(utxos, amount, changeAddress) { return __awaiter(this, void 0, void 0, function* () { const deployTx = new scryptlib_1.bsv.Transaction().from(utxos) .addOutput(new scryptlib_1.bsv.Transaction.Output({ script: this.lockingScript, satoshis: amount, })); deployTx.change(changeAddress); deployTx.feePerKb(yield this.provider.getFeePerKb()); return deployTx; }); } /** * Deploy the contract * @param amount satoshis locked in the contract, 1 sat by default * @param options An optional parameter that can specify the change address and additional UTXOs * @returns The transaction id of the successfully deployed contract */ deploy(amount = 1, options) { return __awaiter(this, void 0, void 0, function* () { this.markAsGenesis(); /** * Use a dummy transaction to estimate fee. * Add two p2pkh inputs trying to avoid re-fetching utxos if the request amount is not enough. */ const changeAddress = (options === null || options === void 0 ? void 0 : options.changeAddress) || (yield this.signer.getDefaultAddress()); const tx = yield this.buildDeployTransaction((options === null || options === void 0 ? void 0 : options.utxos) || [], amount, changeAddress); const address = (options === null || options === void 0 ? void 0 : options.address) || (yield this.signer.getDefaultAddress()); const feePerKb = yield this.provider.getFeePerKb(); yield SmartContract.autoPayfee(tx, feePerKb, this.signer, address); const signedTx = yield this.signer.signAndsendTransaction(tx, { address }); this.from = { tx: signedTx, outputIndex: 0 }; return signedTx; }); } /** @ignore */ _prepareArgsForMethodCall(methodName, ...args) { const scryptMethod = this.getDelegateClazz().abi.find(func => func.name === methodName); if (!scryptMethod) { throw new Error(`\`${this.constructor.name}.${methodName}\` no exists!`); } const methodParamLength = this.getMethodsMeta(methodName).argLength; if (args.length != methodParamLength && args.length != methodParamLength + 1) { throw new Error(`argument count mismatch while calling \`this.methods.${methodName}\`. Expected ${methodParamLength} or ${methodParamLength + 1} (if passing \`MethodCallOptions\`), but got ${args.length}`); } const methodArgs = args.slice(0, methodParamLength); const maybeOptArg = args[methodParamLength]; let methodCallOptions = { verify: false, partiallySigned: false, exec: true, autoPayFee: true, multiContractCall: false, next: [], }; if (maybeOptArg) { // check options argument if (typeof maybeOptArg !== 'object') { throw new Error(`The last argument is expected to be an object of type \`MethodCallOptions\`. Got \`${typeof maybeOptArg}\' instead.`); } Object.assign(methodCallOptions, maybeOptArg); } let sigArgs = methodArgs.reduce((sigsMap, arg, argIdx) => { const scrParam = scryptMethod.params[argIdx]; const match = scrParam.type.match(/^Sig(\[(\d+)\])?$/); const argType = typeof arg; if (match) { const sigLen = match[2] || '1'; if (argType !== 'function') { throw new Error(`a callback function of type \`(sigResponses: SignatureResponse[]) => Sig | Sig[] \` should be given for the Sig type argument`); } sigsMap.set(argIdx, { callback: arg, length: parseInt(sigLen) }); } else { if (argType === 'function') { throw new Error(`a callback function given for the non-Sig type argument is not allowed`); } } return sigsMap; }, new Map()); return { methodArgs, methodCallOptions, sigArgs }; } call(methodName, ...args) { return __awaiter(this, void 0, void 0, function* () { // Reset OP_CODESEPARATOR counter. this._csNum = 0; const { methodArgs, methodCallOptions, sigArgs } = this._prepareArgsForMethodCall(methodName, ...args); if (methodCallOptions.multiContractCall === true) { return this.multiContractCall(methodName, methodCallOptions, methodArgs, sigArgs); } return this.singleContractCall(methodName, methodCallOptions, methodArgs, sigArgs); }); } singleContractCall(methodName, methodCallOptions, methodArgs, sigArgs) { return __awaiter(this, void 0, void 0, function* () { const address = yield this.signer.getDefaultAddress(); const feePerKb = yield this.provider.getFeePerKb(); let txBuilder = this.getTxBuilder(methodName); if (!txBuilder) { throw new Error(`missing bound \`txBuilder\` for calling \`${methodName}\``); } // build a signed dummyTx to estimate fee const methodArgsWithDummySig = methodArgs.map((arg, argIdx) => { const sigArg = sigArgs.get(argIdx); if (sigArg) { // replace sig-related args with dummy values return sigArg.length > 1 ? new Array(sigArg.length).fill((0, utils_3.getDummySig)()) : (0, utils_3.getDummySig)(); } else { return arg; } }); if (!methodCallOptions.fromUTXO && !this.from) { throw new Error(`param \`fromUTXO\` and \`this.from\` should not be both empty for calling \`this.buildContractInput\`!`); } if (methodCallOptions.fromUTXO) { this.from = methodCallOptions.fromUTXO; } const autoPayFee = methodCallOptions.autoPayFee === undefined || methodCallOptions.autoPayFee === true; let { tx, atInputIndex, nexts } = yield txBuilder.call(null, this, methodCallOptions, ...methodArgsWithDummySig); tx.feePerKb(feePerKb); this.to = { tx, inputIndex: atInputIndex }; // Add dummy data to input in order to get the fee estimate right. this.dummySignSingleCallTx(tx, atInputIndex, methodName, ...methodArgsWithDummySig); if (autoPayFee) { // Add fee inputs and adjust change out value. const hasPrevouts = this.hasPrevouts(methodName); yield SmartContract.autoPayfee(tx, feePerKb, this.signer, address, hasPrevouts ? 36 : 0); if (hasPrevouts) { this.dummySignSingleCallTx(tx, atInputIndex, methodName, ...methodArgsWithDummySig); // Only adjust change out value. tx['_feePerKb'] = feePerKb; Object.getPrototypeOf(tx)._updateChangeOutput.apply(tx); } } // re sign use real key. yield this.signSingleCallTx(tx, atInputIndex, address, methodCallOptions, methodName, methodArgs, sigArgs); // bind relations this.to = { tx, inputIndex: atInputIndex };