@moosty/lisk-htlc
Version:
Hashed Time Lock Contract transaction for Lisk SDK based blockchain applications
409 lines • 20.2 kB
JavaScript
"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