UNPKG

@moosty/lisk-htlc

Version:

Hashed Time Lock Contract transaction for Lisk SDK based blockchain applications

409 lines 20.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const bignum_1 = tslib_1.__importDefault(require("@liskhq/bignum")); const lisk_cryptography_1 = require("@liskhq/lisk-cryptography"); const lisk_validator_1 = require("@liskhq/lisk-validator"); const lisk_transactions_1 = require("@liskhq/lisk-transactions"); const schemas_1 = require("./schemas"); const constants_1 = require("./constants"); const utils_1 = require("./utils"); const { verifyAmountBalance, verifyBalance } = lisk_transactions_1.utils; const { BYTESIZES, MAX_TRANSACTION_AMOUNT } = lisk_transactions_1.constants; const schemas = { [constants_1.subTypes.LOCK]: schemas_1.HTLCAssetLockFormatSchema, [constants_1.subTypes.UNLOCK]: schemas_1.HTLCAssetUnlockFormatSchema, [constants_1.subTypes.REFUND]: schemas_1.HTLCAssetRefundFormatSchema, }; class HTLCTransaction extends lisk_transactions_1.BaseTransaction { constructor(rawTransaction, fee) { super(rawTransaction); const tx = (typeof rawTransaction === 'object' && rawTransaction !== null ? rawTransaction : {}); this._id = tx.id; this._subType = this.getTransactionSubType(tx); this.asset = {}; if (this._subType === constants_1.subTypes.LOCK) { const rawAsset = tx.asset; this.fee = new bignum_1.default(fee); this.asset = { recipientPublicKey: rawAsset.recipientPublicKey ? rawAsset.recipientPublicKey : '', amount: BigInt(lisk_validator_1.isPositiveNumberString(rawAsset.amount) ? rawAsset.amount : '0'), type: rawAsset.type, time: rawAsset.time ? rawAsset.time : 0, data: rawAsset.data ? rawAsset.data : '', secretLength: rawAsset.secretLength ? rawAsset.secretLength : 0, contractId: rawAsset.contractId ? rawAsset.contractId : utils_1.getContractAddress(rawAsset, this.senderPublicKey), }; } else if (this._subType === constants_1.subTypes.UNLOCK) { const rawAsset = tx.asset; this.fee = new bignum_1.default(0); this.asset = { contractId: rawAsset.contractId, secret: rawAsset.secret, }; } else if (this._subType === constants_1.subTypes.REFUND) { const rawAsset = tx.asset; this.fee = new bignum_1.default(0); this.asset = { contractId: rawAsset.contractId, data: rawAsset.data, }; } } getTransactionSubType(tx) { if (tx.asset && tx.asset.contractId && tx.asset.data && !tx.asset.amount && !tx.asset.time && !tx.asset.type) { return constants_1.subTypes.REFUND; } else if (tx.asset && tx.asset.contractId && tx.asset.secret) { return constants_1.subTypes.UNLOCK; } else if (tx.asset && tx.asset.type && tx.asset.amount && tx.asset.time) { return constants_1.subTypes.LOCK; } return constants_1.subTypes.UNKNOWN; } assetToBytes() { const transactionAmount = lisk_cryptography_1.bigNumberToBuffer(this.asset.amount ? this.asset.amount.toString() : '0', BYTESIZES.AMOUNT, 'big'); const contractId = this.asset.contractId ? lisk_cryptography_1.intToBuffer(this.asset.contractId.slice(0, -1), 8).slice(0, 8) : Buffer.alloc(0); const timeBuffer = this.asset.time ? lisk_cryptography_1.intToBuffer(this.asset.time, 4) : Buffer.alloc(0); const dataBuffer = this.asset.data ? lisk_cryptography_1.stringToBuffer(this.asset.data) : Buffer.alloc(0); const typeBuffer = this.asset.type ? lisk_cryptography_1.stringToBuffer(this.asset.type) : Buffer.alloc(0); const secretLengthBuffer = this.asset.secretLength ? lisk_cryptography_1.intToBuffer(this.asset.secretLength, 3) : Buffer.alloc(0); const secretBuffer = this.asset.secret ? lisk_cryptography_1.stringToBuffer(this.asset.secret) : Buffer.alloc(0); const recipientBuffer = this.asset.recipientPublicKey ? lisk_cryptography_1.stringToBuffer(this.asset.recipientPublicKey) : Buffer.alloc(0); return Buffer.concat([ transactionAmount, contractId, recipientBuffer, typeBuffer, dataBuffer, secretLengthBuffer, secretBuffer, timeBuffer, ]); } assetToJSON() { switch (this._subType) { case constants_1.subTypes.LOCK: return { contractId: this.asset.contractId, amount: this.asset.amount.toString(), recipientPublicKey: this.asset.recipientPublicKey, type: this.asset.type, time: this.asset.time, data: this.asset.data, secretLength: this.asset.secretLength, }; case constants_1.subTypes.UNLOCK: return { contractId: this.asset.contractId, secret: this.asset.secret, }; case constants_1.subTypes.REFUND: return { contractId: this.asset.contractId, data: this.asset.data, }; default: return {}; } } async prepare(store) { const cacheArray = [ { address: this.senderId, } ]; if (this.asset.contractId) { cacheArray.push({ address: this.asset.contractId, }); } if (this.asset.recipientPublicKey) { cacheArray.push({ address: lisk_cryptography_1.getAddressFromPublicKey(this.asset.recipientPublicKey), }); } await store.account.cache(cacheArray); } validateAsset() { if (this._subType === constants_1.subTypes.UNKNOWN) { return [ new lisk_transactions_1.TransactionError('Couldn\'t match a sub type.', this.id, '.asset.??'), ]; } const asset = this.assetToJSON(); const schemaErrors = lisk_validator_1.validator.validate(schemas[this._subType], asset); const errors = lisk_transactions_1.convertToAssetError(this.id, schemaErrors); if (this._subType === constants_1.subTypes.LOCK) { const contractAddressError = utils_1.verifyContractAddress(this.id, this.asset, this.senderPublicKey); if (contractAddressError) { errors.push(contractAddressError); } const secondsSinceEpoch = Math.round(new Date().getTime() / 1000); if (!this.blockId && this.asset.time && secondsSinceEpoch + constants_1.MIN_LOCK_TIME > this.asset.time) { errors.push(new lisk_transactions_1.TransactionError('`time` is already passed.', this.id, '.asset.time', this.asset.time.toString(), `> NOW + ${constants_1.MIN_LOCK_TIME}`)); } if (this.asset.amount && !lisk_validator_1.isValidTransferAmount(this.asset.amount.toString())) { errors.push(new lisk_transactions_1.TransactionError('Amount must be a valid number in string format.', this.id, '.asset.amount', this.asset.amount.toString())); } if (!this.asset.recipientPublicKey) { errors.push(new lisk_transactions_1.TransactionError('`recipientPublicKey` must be provided.', this.id, '.asset.recipientPublicKey')); } } return errors; } applyAsset(store) { switch (this._subType) { case constants_1.subTypes.UNLOCK: return this._applyRedeemAsset(store); case constants_1.subTypes.REFUND: return this._applyRefundAsset(store); default: return this._applyLockAsset(store); } } _applyLockAsset(store) { const errors = []; const contract = store.account.getOrDefault(this.asset.contractId); const contractExist = (contract && !!contract.publicKey) || (contract && BigInt(contract.balance) > 0) || (contract && Object.entries(contract.asset).length > 0); const sender = store.account.get(this.senderId); if (contractExist) { errors.push(new lisk_transactions_1.TransactionError('`contractId` exists already.', this.id, '.contractId', this.asset.contractId)); } const balanceError = verifyAmountBalance(this.id, sender, this.asset.amount, this.fee); if (balanceError) { errors.push(balanceError); } const updatedSenderBalance = BigInt(sender.balance) - BigInt(this.asset.amount); const updatedSender = { ...sender, balance: updatedSenderBalance.toString(), }; store.account.set(updatedSender.address, updatedSender); const updatedContract = { ...contract, balance: BigInt(this.asset.amount).toString(), publicKey: utils_1.assetsToPublicKey(this.asset, this.senderPublicKey), asset: { type: this.asset.type, hash: this.asset.data, time: this.asset.time, length: this.asset.secretLength, amount: this.asset.amount.toString(), recipientPublicKey: this.asset.recipientPublicKey, senderPublicKey: this.senderPublicKey, }, }; store.account.set(updatedContract.address, updatedContract); return errors; } _applyRedeemAsset(store) { const errors = []; const contract = store.account.getOrDefault(this.asset.contractId); const contractExist = (contract && !!contract.publicKey) || (contract && BigInt(contract.balance) > 0) || (contract && Object.entries(contract.asset).length > 0); const secondsSinceEpoch = Math.round(new Date().getTime() / 1000); if (!contractExist) { errors.push(new lisk_transactions_1.TransactionError('Contract doesn\'t exist.', this.id, this.asset.contractId)); } else { const { senderPublicKey, recipientPublicKey, time, type, hash, key, timedOut } = contract.asset; if (senderPublicKey !== this.senderPublicKey && recipientPublicKey !== this.senderPublicKey) { errors.push(new lisk_transactions_1.TransactionError('`senderPublicKey` is not a participant in this contract.', this.id, '.senderPublicKey', this.senderPublicKey)); } if (key || timedOut) { errors.push(new lisk_transactions_1.TransactionError('Contract already resolved.', this.id, key ? '.asset.key' : '.asset.timedOut', key ? `${key}` : `${timedOut}`)); } const sender = store.account.getOrDefault(this.senderId); if (!this.blockId && secondsSinceEpoch > time) { errors.push(new lisk_transactions_1.TransactionError('Contract is timed out.', this.id, '.asset.time', time, `< ${secondsSinceEpoch}`)); } if (!utils_1.verifyKey(hash, this.asset.secret, type)) { errors.push(new lisk_transactions_1.TransactionError('Wrong secret.', this.id, '.asset.secret', type === "OP_HASH256" ? `SHA256(${this.asset.secret}) == ${utils_1.hashKey(this.asset.secret, type)}` : `RIPEMD160(SHA256(${this.asset.secret})) == ${utils_1.hashKey(this.asset.secret, type)}`, type === "OP_HASH256" ? `sha256(${this.asset.secret}) == ${hash}` : `RIPEMD160(sha256(${this.asset.secret})) == ${hash}`)); } const balanceError = verifyAmountBalance(this.id, contract, new bignum_1.default(contract.asset.amount), this.fee); if (balanceError) { errors.push(balanceError); } const updatedContract = { ...contract, balance: '0', asset: { ...contract.asset, key: this.asset.secret, }, }; store.account.set(updatedContract.address, updatedContract); const updatedRecipientBalance = BigInt(sender.balance) + BigInt(contract.balance); if (updatedRecipientBalance > BigInt(MAX_TRANSACTION_AMOUNT)) { errors.push(new lisk_transactions_1.TransactionError('Max transaction amount reached', this.id, '.amount', updatedRecipientBalance.toString())); } const updatedRecipient = { ...sender, balance: updatedRecipientBalance.toString(), }; store.account.set(updatedRecipient.address, updatedRecipient); } return errors; } _applyRefundAsset(store) { const errors = []; const contract = store.account.getOrDefault(this.asset.contractId); const contractExist = (contract && !!contract.publicKey) || (contract && BigInt(contract.balance) > 0) || (contract && Object.entries(contract.asset).length > 0); const secondsSinceEpoch = Math.round(new Date().getTime() / 1000); if (!contractExist) { errors.push(new lisk_transactions_1.TransactionError('Contract doesn\'t exist.', this.id, this.asset.contractId)); } else { const { senderPublicKey, recipientPublicKey, time, hash, key, timedOut } = contract.asset; if (senderPublicKey !== this.senderPublicKey && recipientPublicKey !== this.senderPublicKey) { errors.push(new lisk_transactions_1.TransactionError('`senderPublicKey` is not a participant in this contract.', this.id, '.senderPublicKey', this.senderPublicKey)); } if (key || timedOut) { errors.push(new lisk_transactions_1.TransactionError('Contract already resolved.', this.id, key ? '.asset.key' : '.asset.timedOut', key ? `${key}` : `${timedOut}`)); } const sender = store.account.get(this.senderId); if (secondsSinceEpoch < time) { errors.push(new lisk_transactions_1.TransactionError('Contract is not yet timed out.', this.id, '.asset.time', time, `>${secondsSinceEpoch}`)); } if (hash !== this.asset.data) { errors.push(new lisk_transactions_1.TransactionError('`data` is not correct.', this.id, '.asset.data', this.asset.data, hash)); } const balanceError = verifyAmountBalance(this.id, contract, new bignum_1.default(contract.asset.amount), this.fee); if (balanceError) { errors.push(balanceError); } const updatedContract = { ...contract, balance: '0', asset: { ...contract.asset, timedOut: true, }, }; store.account.set(updatedContract.address, updatedContract); const updatedSenderBalance = BigInt(sender.balance) + BigInt(contract.asset.amount); if (updatedSenderBalance > BigInt(MAX_TRANSACTION_AMOUNT)) { errors.push(new lisk_transactions_1.TransactionError('Max transaction amount reached', this.id, '.amount', updatedSenderBalance.toString())); } const updatedSender = { ...sender, balance: updatedSenderBalance.toString(), }; store.account.set(updatedSender.address, updatedSender); } return errors; } undoAsset(store) { switch (this._subType) { case constants_1.subTypes.UNLOCK: return this._undoRedeemAsset(store); case constants_1.subTypes.REFUND: return this._undoRefundAsset(store); default: return this._undoLockAsset(store); } } _undoLockAsset(store) { const errors = []; const sender = store.account.get(this.senderId); const contract = store.account.getOrDefault(this.asset.contractId); const balanceError = verifyBalance(this.id, contract, this.asset.amount); if (balanceError) { errors.push(balanceError); } const updatedSenderBalance = BigInt(sender.balance) + BigInt(this.asset.amount); if (updatedSenderBalance > BigInt(MAX_TRANSACTION_AMOUNT)) { errors.push(new lisk_transactions_1.TransactionError('Invalid amount', this.id, '.amount', updatedSenderBalance.toString())); } const updatedSender = { ...sender, balance: updatedSenderBalance.toString(), }; store.account.set(updatedSender.address, updatedSender); const updatedContract = { address: contract.address, publicKey: '', balance: '0', asset: {}, }; store.account.set(updatedContract.address, updatedContract); return errors; } _undoRedeemAsset(store) { const errors = []; const sender = store.account.get(this.senderId); const contract = store.account.getOrDefault(this.asset.contractId); const { key } = contract.asset; if (key) { const balanceError = verifyBalance(this.id, sender, new bignum_1.default(contract.asset.amount)); if (balanceError) { errors.push(balanceError); } const updatedContractBalance = BigInt(contract.balance) + BigInt(contract.asset.amount); if (updatedContractBalance > BigInt(MAX_TRANSACTION_AMOUNT)) { errors.push(new lisk_transactions_1.TransactionError('Invalid amount', this.id, '.asset.amount', updatedContractBalance.toString())); } const updatedContract = { ...contract, balance: updatedContractBalance.toString(), asset: { ...contract.asset, key: null, }, }; store.account.set(updatedContract.address, updatedContract); const updatedRecipient = { ...sender, balance: (BigInt(sender.balance) - BigInt(contract.balance)).toString(), }; store.account.set(sender.address, updatedRecipient); } return errors; } _undoRefundAsset(store) { const errors = []; const sender = store.account.get(this.senderId); const contract = store.account.getOrDefault(this.asset.contractId); const { timedOut } = contract.asset; if (timedOut) { const balanceError = verifyBalance(this.id, sender, new bignum_1.default(contract.asset.amount)); if (balanceError) { errors.push(balanceError); } const updatedContractBalance = BigInt(contract.balance) + BigInt(contract.asset.amount); if (updatedContractBalance > BigInt(MAX_TRANSACTION_AMOUNT)) { errors.push(new lisk_transactions_1.TransactionError('Invalid amount', this.id, '.amount', updatedContractBalance.toString())); } const updatedContract = { ...contract, balance: updatedContractBalance.toString(), asset: { ...contract.asset, timedOut: false, }, }; store.account.set(updatedContract.address, updatedContract); const updatedSender = { ...sender, balance: (BigInt(sender.balance) - BigInt(contract.balance)).toString(), }; store.account.set(updatedSender.address, updatedSender); } return errors; } } exports.HTLCTransaction = HTLCTransaction; HTLCTransaction.TYPE = 199; HTLCTransaction.FEE = constants_1.HTLC_FEE.toString(); //# sourceMappingURL=htlc.js.map