Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
const {
BaseTransaction,
TransactionError,
utils,
} = require('@liskhq/lisk-transactions');
const _ = require('lodash');
/**
* type asset {
* recipients: Recipient[]
* }
* type Recipient {
* address: <lisk_address>,
* amount: BigNum string,
* }
*/
class MultiTransfer extends BaseTransaction {
static get TYPE() {
return 42;
}
static get FEE() {
return `${10 ** 8}`;
};
validateAsset() {
const transaction = this;
const errors = [];
const minimumRecipientsCount = 2;
const maximumRecipientsCount = 10;
const errorRecipientMustBeBlankAddress = new TransactionError("The recipient must be 0L for this transaction", this.id, ".recipientId" , this.recipientId, "0L");
const errorNotAnArray = new TransactionError("The asset must be an array of Recipients", this.id, ".asset.recipients" , this.asset.recipients, "an array of recipients");
const errorTooFewRecipients = new TransactionError(
`You must provide at least ${minimumRecipientsCount} recipients`, this.id, ".asset.recipients" , this.asset.recipients, `at least ${minimumRecipientsCount} recipients`);
const errorTooManyRecipients = new TransactionError(
`You must provide at most ${maximumRecipientsCount} recipients`, this.id, ".asset.recipients" , this.asset.recipients, `at most ${maximumRecipientsCount} recipients`);
const errorDuplicates = new TransactionError("Each recipient must appear at most once", this.id, ".asset.recipients" , this.asset.recipients, "a set of unique recipients");
const errorSelfRecipient = new TransactionError("The sender cannot be one of the recipients", this.id, ".asset.recipients" , this.asset.recipients);
const errorInvalidAddressBuilder = (recipientIndex, givenAddress, validationError) => new TransactionError(`Invalid address of recipient #${recipientIndex} : ${validationError}`, this.id, ".asset.recipients[*].address", givenAddress, "a valid Lisk address");
const errorInvalidAmountBuilder = (recipientIndex, givenAmount) => new TransactionError(`Invalid transfer amount for recipient #${recipientIndex}`, this.id, ".asset.recipients[*].amount", givenAmount, "a valid BigNumber.toString() amount");
if (this.recipientId !== '0L') {
errors.push(errorRecipientMustBeBlankAddress);
}
if (!_.isArray(this.asset.recipients)) {
errors.push(errorNotAnArray);
return errors;
}
// From here this.asset.recipients is an actual array
if (this.asset.recipients.length < minimumRecipientsCount) {
errors.push(errorTooFewRecipients);
}
if (this.asset.recipients.length > maximumRecipientsCount) {
errors.push(errorTooManyRecipients);
}
const uniqueRecipients = _.uniqBy(this.asset.recipients, 'address');
if (uniqueRecipients.length !== this.asset.recipients.length) {
errors.push(errorDuplicates);
}
// Check the format of each recipient
this.asset.recipients.forEach((recipient, index) => {
// Validate the address
try {
utils.validateAddress(recipient.address);
} catch(e) {
errors.push(errorInvalidAddressBuilder(index, recipient.address, e.message));
}
// Validate the amount is a valid BigNum
try {
new utils.BigNum(recipient.amount);
if (!utils.validateTransferAmount(recipient.amount)) {
throw new Error();
}
} catch (e) {
errors.push(errorInvalidAmountBuilder(index, recipient.amount));
}
});
const selfRecipient = this.asset.recipients.find(r => r.address === transaction.senderId);
if (selfRecipient) {
errors.push(errorSelfRecipient);
}
return errors;
}
async prepare(store) {
const accountsToFetch = this.asset.recipients.map(r => ({address: r.address}));
accountsToFetch.push({address: this.senderId});
await store.account.cache(accountsToFetch);
}
applyAsset(store) {
const errors = [];
// Let's validate the sender is rich enough
const sender = store.account.get(this.senderId);
const totalAmountToTransfer = this.asset.recipients.reduce((total, r) => {
return total.add(r.amount);
}, new utils.BigNum(0));
const senderBalanceValidationError = utils.verifyAmountBalance(this.id, sender, totalAmountToTransfer, this.fee);
if (senderBalanceValidationError) {
errors.push(senderBalanceValidationError);
}
if (errors.length > 0) {
return errors;
}
this.asset.recipients.forEach(r => {
const { address, amount } = r;
const recipient = store.account.get(address);
const bigNumAmount = new utils.BigNum(amount);
recipient.balance = new utils.BigNum(recipient.balance).add(bigNumAmount).toString();
store.account.set(address, recipient);
});
sender.balance = new utils.BigNum(sender.balance).sub(totalAmountToTransfer).toString();
store.account.set(sender.address, sender);
return errors;
}
undoAsset(store) {
const errors = [];
// Here in undoAsset() I assume every account has a sufficient balance since it's a rollback
const sender = store.account.get(this.senderId);
const totalAmountToTransfer = this.asset.recipients.reduce((total, r) => {
return total.add(r.amount);
}, new utils.BigNum(0));
this.asset.recipients.forEach(r => {
const { address, amount } = r;
const recipient = store.account.get(address);
const bigNumAmount = new utils.BigNum(amount);
recipient.balance = new utils.BigNum(recipient.balance).sub(bigNumAmount).toString();
store.account.set(address, recipient);
});
sender.balance = new utils.BigNum(sender.balance).add(totalAmountToTransfer).toString();
store.account.set(sender.address, sender);
return errors;
}
}
module.exports = MultiTransfer;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment