diff --git a/e2e/service/TransactionService.spec.ts b/e2e/service/TransactionService.spec.ts new file mode 100644 index 0000000000..060d3dfe5d --- /dev/null +++ b/e2e/service/TransactionService.spec.ts @@ -0,0 +1,561 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { assert } from 'chai'; +import { expect } from 'chai'; +import { Convert } from '../../src/core/format/Convert'; +import { Listener } from '../../src/infrastructure/Listener'; +import { NamespaceHttp } from '../../src/infrastructure/NamespaceHttp'; +import { TransactionHttp } from '../../src/infrastructure/TransactionHttp'; +import { Account } from '../../src/model/account/Account'; +import { Address } from '../../src/model/account/Address'; +import { NetworkType } from '../../src/model/blockchain/NetworkType'; +import { PlainMessage } from '../../src/model/message/PlainMessage'; +import { Mosaic } from '../../src/model/mosaic/Mosaic'; +import { MosaicFlags } from '../../src/model/mosaic/MosaicFlags'; +import { MosaicId } from '../../src/model/mosaic/MosaicId'; +import { MosaicNonce } from '../../src/model/mosaic/MosaicNonce'; +import { MosaicSupplyChangeAction } from '../../src/model/mosaic/MosaicSupplyChangeAction'; +import { NetworkCurrencyMosaic } from '../../src/model/mosaic/NetworkCurrencyMosaic'; +import { AliasAction } from '../../src/model/namespace/AliasAction'; +import { NamespaceId } from '../../src/model/namespace/NamespaceId'; +import { AddressAliasTransaction } from '../../src/model/transaction/AddressAliasTransaction'; +import { AggregateTransaction } from '../../src/model/transaction/AggregateTransaction'; +import { Deadline } from '../../src/model/transaction/Deadline'; +import { MosaicAliasTransaction } from '../../src/model/transaction/MosaicAliasTransaction'; +import { MosaicDefinitionTransaction } from '../../src/model/transaction/MosaicDefinitionTransaction'; +import { MosaicMetadataTransaction } from '../../src/model/transaction/MosaicMetadataTransaction'; +import { MosaicSupplyChangeTransaction } from '../../src/model/transaction/MosaicSupplyChangeTransaction'; +import { NamespaceRegistrationTransaction } from '../../src/model/transaction/NamespaceRegistrationTransaction'; +import { SignedTransaction } from '../../src/model/transaction/SignedTransaction'; +import { TransferTransaction } from '../../src/model/transaction/TransferTransaction'; +import { UInt64 } from '../../src/model/UInt64'; +import { TransactionService } from '../../src/service/TransactionService'; + +describe('TransactionService', () => { + let account: Account; + let account2: Account; + let account3: Account; + let account4: Account; + let url: string; + let generationHash: string; + let namespaceHttp: NamespaceHttp; + let addressAlias: NamespaceId; + let mosaicAlias: NamespaceId; + let mosaicId: MosaicId; + let newMosaicId: MosaicId; + let transactionHttp: TransactionHttp; + let config; + let transactionHashes: string[]; + let transactionHashesMultiple: string[]; + + before((done) => { + const path = require('path'); + require('fs').readFile(path.resolve(__dirname, '../conf/network.conf'), (err, data) => { + if (err) { + throw err; + } + const json = JSON.parse(data); + config = json; + account = Account.createFromPrivateKey(json.testAccount.privateKey, NetworkType.MIJIN_TEST); + account2 = Account.createFromPrivateKey(json.testAccount2.privateKey, NetworkType.MIJIN_TEST); + account3 = Account.createFromPrivateKey(json.testAccount3.privateKey, NetworkType.MIJIN_TEST); + account4 = Account.createFromPrivateKey(json.cosignatory4Account.privateKey, NetworkType.MIJIN_TEST); + url = json.apiUrl; + generationHash = json.generationHash; + namespaceHttp = new NamespaceHttp(json.apiUrl); + transactionHttp = new TransactionHttp(json.apiUrl); + transactionHashes = []; + transactionHashesMultiple = []; + done(); + }); + }); + + /** + * ========================= + * Setup test data + * ========================= + */ + describe('Create address alias NamespaceId', () => { + let listener: Listener; + before (() => { + listener = new Listener(config.apiUrl); + return listener.open(); + }); + after(() => { + return listener.close(); + }); + it('Announce NamespaceRegistrationTransaction', (done) => { + const namespaceName = 'root-test-namespace-' + Math.floor(Math.random() * 10000); + const registerNamespaceTransaction = NamespaceRegistrationTransaction.createRootNamespace( + Deadline.create(), + namespaceName, + UInt64.fromUint(20), + NetworkType.MIJIN_TEST, + ); + addressAlias = new NamespaceId(namespaceName); + const signedTransaction = registerNamespaceTransaction.signWith(account, generationHash); + transactionHashes.push(signedTransaction.hash); + listener.confirmed(account.address).subscribe(() => { + done(); + }); + listener.status(account.address).subscribe((error) => { + console.log('Error:', error); + assert(false); + done(); + }); + transactionHttp.announce(signedTransaction); + }); + }); + + describe('Create mosaic alias NamespaceId', () => { + let listener: Listener; + before (() => { + listener = new Listener(config.apiUrl); + return listener.open(); + }); + after(() => { + return listener.close(); + }); + it('Announce NamespaceRegistrationTransaction', (done) => { + const namespaceName = 'root-test-namespace-' + Math.floor(Math.random() * 10000); + const registerNamespaceTransaction = NamespaceRegistrationTransaction.createRootNamespace( + Deadline.create(), + namespaceName, + UInt64.fromUint(50), + NetworkType.MIJIN_TEST, + ); + mosaicAlias = new NamespaceId(namespaceName); + const signedTransaction = registerNamespaceTransaction.signWith(account, generationHash); + transactionHashes.push(signedTransaction.hash); + listener.confirmed(account.address).subscribe(() => { + done(); + }); + listener.status(account.address).subscribe((error) => { + console.log('Error:', error); + assert(false); + done(); + }); + transactionHttp.announce(signedTransaction); + }); + }); + + describe('Setup test AddressAlias', () => { + let listener: Listener; + before (() => { + listener = new Listener(config.apiUrl); + return listener.open(); + }); + after(() => { + return listener.close(); + }); + + it('Announce addressAliasTransaction', (done) => { + const addressAliasTransaction = AddressAliasTransaction.create( + Deadline.create(), + AliasAction.Link, + addressAlias, + account.address, + NetworkType.MIJIN_TEST, + ); + const signedTransaction = addressAliasTransaction.signWith(account, generationHash); + transactionHashes.push(signedTransaction.hash); + listener.confirmed(account.address).subscribe(() => { + done(); + }); + listener.status(account.address).subscribe((error) => { + console.log('Error:', error); + assert(false); + done(); + }); + transactionHttp.announce(signedTransaction); + }); + }); + + describe('Setup test MosaicId', () => { + let listener: Listener; + before (() => { + listener = new Listener(config.apiUrl); + return listener.open(); + }); + after(() => { + return listener.close(); + }); + it('Announce MosaicDefinitionTransaction', (done) => { + const nonce = MosaicNonce.createRandom(); + mosaicId = MosaicId.createFromNonce(nonce, account.publicAccount); + const mosaicDefinitionTransaction = MosaicDefinitionTransaction.create( + Deadline.create(), + nonce, + mosaicId, + MosaicFlags.create(true, true, false), + 3, + UInt64.fromUint(50), + NetworkType.MIJIN_TEST, + ); + const signedTransaction = mosaicDefinitionTransaction.signWith(account, generationHash); + transactionHashes.push(signedTransaction.hash); + listener.confirmed(account.address).subscribe(() => { + done(); + }); + transactionHttp.announce(signedTransaction); + }); + }); + + describe('MosaicSupplyChangeTransaction', () => { + let listener: Listener; + before (() => { + listener = new Listener(config.apiUrl); + return listener.open(); + }); + after(() => { + return listener.close(); + }); + it('standalone', (done) => { + const mosaicSupplyChangeTransaction = MosaicSupplyChangeTransaction.create( + Deadline.create(), + mosaicId, + MosaicSupplyChangeAction.Increase, + UInt64.fromUint(200000), + NetworkType.MIJIN_TEST, + ); + const signedTransaction = mosaicSupplyChangeTransaction.signWith(account, generationHash); + transactionHashes.push(signedTransaction.hash); + listener.confirmed(account.address).subscribe(() => { + done(); + }); + listener.status(account.address).subscribe((error) => { + console.log('Error:', error); + assert(false); + done(); + }); + transactionHttp.announce(signedTransaction); + }); + }); + + describe('Setup MosaicAlias', () => { + let listener: Listener; + before (() => { + listener = new Listener(config.apiUrl); + return listener.open(); + }); + after(() => { + return listener.close(); + }); + + it('Announce MosaicAliasTransaction', (done) => { + const mosaicAliasTransaction = MosaicAliasTransaction.create( + Deadline.create(), + AliasAction.Link, + mosaicAlias, + mosaicId, + NetworkType.MIJIN_TEST, + ); + const signedTransaction = mosaicAliasTransaction.signWith(account, generationHash); + transactionHashes.push(signedTransaction.hash); + listener.confirmed(account.address).subscribe(() => { + done(); + }); + listener.status(account.address).subscribe((error) => { + console.log('Error:', error); + assert(false); + done(); + }); + transactionHttp.announce(signedTransaction); + }); + }); + describe('Create Transfer with alias', () => { + let listener: Listener; + before (() => { + listener = new Listener(config.apiUrl); + return listener.open(); + }); + after(() => { + return listener.close(); + }); + + it('Announce TransferTransaction', (done) => { + const transferTransaction = TransferTransaction.create( + Deadline.create(), + addressAlias, + [ + NetworkCurrencyMosaic.createAbsolute(1), + new Mosaic(mosaicAlias, UInt64.fromUint(1)), + ], + PlainMessage.create('test-message'), + NetworkType.MIJIN_TEST, + ); + const signedTransaction = transferTransaction.signWith(account, generationHash); + + transactionHashes.push(signedTransaction.hash); + + listener.confirmed(account.address).subscribe(() => { + done(); + }); + listener.status(account.address).subscribe((error) => { + console.log('Error:', error); + assert(false); + done(); + }); + transactionHttp.announce(signedTransaction); + }); + }); + + /** + * ===================================== + * Setup test aggregate transaction data + * ===================================== + */ + + describe('Create Aggreate TransferTransaction', () => { + let listener: Listener; + before (() => { + listener = new Listener(config.apiUrl); + return listener.open(); + }); + after(() => { + return listener.close(); + }); + it('aggregate', (done) => { + const signedTransaction = buildAggregateTransaction().signWith(account, generationHash); + transactionHashes.push(signedTransaction.hash); + listener.confirmed(account.address).subscribe(() => { + done(); + }); + listener.status(account.address).subscribe((error) => { + console.log('Error:', error); + assert(false); + done(); + }); + transactionHttp.announce(signedTransaction); + }); + }); + + /** + * ===================================== + * Setup test Multiple transaction on same block + * ===================================== + */ + + describe('Transfer mosaic to account 3', () => { + let listener: Listener; + before (() => { + listener = new Listener(config.apiUrl); + return listener.open(); + }); + after(() => { + return listener.close(); + }); + + it('Announce TransferTransaction', (done) => { + const transferTransaction = TransferTransaction.create( + Deadline.create(), + account3.address, + [ + new Mosaic(mosaicAlias, UInt64.fromUint(200)), + ], + PlainMessage.create('test-message'), + NetworkType.MIJIN_TEST, + ); + const signedTransaction = transferTransaction.signWith(account, generationHash); + + transactionHashes.push(signedTransaction.hash); + + listener.confirmed(account.address).subscribe(() => { + done(); + }); + listener.status(account.address).subscribe((error) => { + console.log('Error:', error); + assert(false); + done(); + }); + transactionHttp.announce(signedTransaction); + }); + }); + + describe('Create multiple transfers with alias', () => { + let listener: Listener; + before (() => { + listener = new Listener(config.apiUrl); + return listener.open(); + }); + after(() => { + return listener.close(); + }); + + it('Announce TransferTransaction', (done) => { + const transactions: SignedTransaction[] = []; + // 1. Transfer A -> B + const transaction1 = TransferTransaction.create( + Deadline.create(), + account2.address, + [ + new Mosaic(mosaicAlias, UInt64.fromUint(1)), + ], + PlainMessage.create('test-message'), + NetworkType.MIJIN_TEST, + ); + transactions.push(transaction1.signWith(account, generationHash)); + + // 2. Transfer C -> D + const transaction2 = TransferTransaction.create( + Deadline.create(), + account4.address, + [ + new Mosaic(mosaicAlias, UInt64.fromUint(1)), + ], + PlainMessage.create('test-message'), + NetworkType.MIJIN_TEST, + ); + transactions.push(transaction2.signWith(account3, generationHash)); + + // 3. Aggregate + const lastSignedTx = buildAggregateTransaction().signWith(account, generationHash); + transactions.push(lastSignedTx); + + transactions.forEach((tx) => { + transactionHashesMultiple.push(tx.hash); + transactionHttp.announce(tx); + }); + listener.confirmed(account.address, lastSignedTx.hash).subscribe(() => { + done(); + }); + listener.status(account.address).subscribe((error) => { + console.log('Error:', error); + assert(false); + done(); + }); + listener.status(account3.address).subscribe((error) => { + console.log('Error:', error); + assert(false); + done(); + }); + }); + }); + + /** + * ========================= + * Test + * ========================= + */ + + describe('should return resolved transaction', () => { + it('call transaction service', (done) => { + const transactionService = new TransactionService(url); + transactionService.resolveAliases(transactionHashes).subscribe((transactions) => { + expect(transactions.length).to.be.equal(8); + transactions.map((tx) => { + if (tx instanceof TransferTransaction) { + expect((tx.recipientAddress as Address).plain()).to.be.equal(account.address.plain()); + expect(tx.mosaics.find((m) => m.id.toHex() === mosaicId.toHex())).not.to.equal(undefined); + } else if (tx instanceof AggregateTransaction) { + expect(tx.innerTransactions.length).to.be.equal(5); + // Assert Transfer + expect(((tx.innerTransactions[0] as TransferTransaction).recipientAddress as Address) + .plain()).to.be.equal(account.address.plain()); + expect((tx.innerTransactions[0] as TransferTransaction).mosaics + .find((m) => m.id.toHex() === mosaicId.toHex())).not.to.equal(undefined); + // Assert MosaicMeta + expect((tx.innerTransactions[4] as MosaicMetadataTransaction) + .targetMosaicId.toHex() === newMosaicId.toHex()).to.be.true; + } + }); + }, + done()); + }); + }); + + describe('Test resolve alias with multiple transaction in single block', () => { + it('call transaction service', (done) => { + const transactionService = new TransactionService(url); + transactionService.resolveAliases(transactionHashesMultiple).subscribe((tx) => { + expect(tx.length).to.be.equal(3); + expect((tx[0] as TransferTransaction).mosaics[0].id.toHex()).to.be.equal(mosaicId.toHex()); + expect((tx[1] as TransferTransaction).mosaics[0].id.toHex()).to.be.equal(mosaicId.toHex()); + expect(((tx[2] as AggregateTransaction).innerTransactions[4] as MosaicMetadataTransaction) + .targetMosaicId.toHex()).to.be.equal(newMosaicId.toHex()); + }, + done()); + }); + }); + + function buildAggregateTransaction(): AggregateTransaction { + const transferTransaction = TransferTransaction.create( + Deadline.create(), + addressAlias, + [ + NetworkCurrencyMosaic.createAbsolute(1), + new Mosaic(mosaicAlias, UInt64.fromUint(1)), + ], + PlainMessage.create('test-message'), + NetworkType.MIJIN_TEST, + ); + // Unlink MosaicAlias + const mosaicAliasTransactionUnlink = MosaicAliasTransaction.create( + Deadline.create(), + AliasAction.Unlink, + mosaicAlias, + mosaicId, + NetworkType.MIJIN_TEST, + ); + + // Create a new Mosaic + const nonce = MosaicNonce.createRandom(); + newMosaicId = MosaicId.createFromNonce(nonce, account.publicAccount); + const mosaicDefinitionTransaction = MosaicDefinitionTransaction.create( + Deadline.create(), + nonce, + newMosaicId, + MosaicFlags.create(true, true, false), + 3, + UInt64.fromUint(0), + NetworkType.MIJIN_TEST, + ); + + // Link namespace with new MosaicId + const mosaicAliasTransactionRelink = MosaicAliasTransaction.create( + Deadline.create(), + AliasAction.Link, + mosaicAlias, + newMosaicId, + NetworkType.MIJIN_TEST, + ); + + // Use new mosaicAlias in metadata + const mosaicMetadataTransaction = MosaicMetadataTransaction.create( + Deadline.create(), + account.publicKey, + UInt64.fromUint(5), + mosaicAlias, + 10, + Convert.uint8ToUtf8(new Uint8Array(10)), + NetworkType.MIJIN_TEST, + ); + return AggregateTransaction.createComplete(Deadline.create(), + [ + transferTransaction.toAggregate(account.publicAccount), + mosaicAliasTransactionUnlink.toAggregate(account.publicAccount), + mosaicDefinitionTransaction.toAggregate(account.publicAccount), + mosaicAliasTransactionRelink.toAggregate(account.publicAccount), + mosaicMetadataTransaction.toAggregate(account.publicAccount), + + ], + NetworkType.MIJIN_TEST, + [], + ); + } + +}); diff --git a/e2e/service/TransactionService_AggregateBonded.spec.ts b/e2e/service/TransactionService_AggregateBonded.spec.ts new file mode 100644 index 0000000000..780ccb8d49 --- /dev/null +++ b/e2e/service/TransactionService_AggregateBonded.spec.ts @@ -0,0 +1,292 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { Listener } from '../../src/infrastructure/Listener'; +import { NamespaceHttp } from '../../src/infrastructure/NamespaceHttp'; +import { TransactionHttp } from '../../src/infrastructure/TransactionHttp'; +import { Account } from '../../src/model/account/Account'; +import { Address } from '../../src/model/account/Address'; +import { NetworkType } from '../../src/model/blockchain/NetworkType'; +import { PlainMessage } from '../../src/model/message/PlainMessage'; +import { Mosaic } from '../../src/model/mosaic/Mosaic'; +import { MosaicId } from '../../src/model/mosaic/MosaicId'; +import { NetworkCurrencyMosaic } from '../../src/model/mosaic/NetworkCurrencyMosaic'; +import { NamespaceId } from '../../src/model/namespace/NamespaceId'; +import { AggregateTransaction } from '../../src/model/transaction/AggregateTransaction'; +import { Deadline } from '../../src/model/transaction/Deadline'; +import { LockFundsTransaction } from '../../src/model/transaction/LockFundsTransaction'; +import { MultisigAccountModificationTransaction } from '../../src/model/transaction/MultisigAccountModificationTransaction'; +import { TransactionType } from '../../src/model/transaction/TransactionType'; +import { TransferTransaction } from '../../src/model/transaction/TransferTransaction'; +import { UInt64 } from '../../src/model/UInt64'; +import { TransactionService } from '../../src/service/TransactionService'; +import { TransactionUtils } from '../infrastructure/TransactionUtils'; + +describe('TransactionService', () => { + let account: Account; + let account2: Account; + let multisigAccount: Account; + let cosignAccount1: Account; + let cosignAccount2: Account; + let cosignAccount3: Account; + let networkCurrencyMosaicId: MosaicId; + let url: string; + let generationHash: string; + let transactionHttp: TransactionHttp; + let transactionService: TransactionService; + let namespaceHttp: NamespaceHttp; + let config; + + before((done) => { + const path = require('path'); + require('fs').readFile(path.resolve(__dirname, '../conf/network.conf'), (err, data) => { + if (err) { + throw err; + } + const json = JSON.parse(data); + config = json; + account = Account.createFromPrivateKey(json.testAccount.privateKey, NetworkType.MIJIN_TEST); + account2 = Account.createFromPrivateKey(json.testAccount2.privateKey, NetworkType.MIJIN_TEST); + multisigAccount = Account.createFromPrivateKey(json.multisigAccount.privateKey, NetworkType.MIJIN_TEST); + cosignAccount1 = Account.createFromPrivateKey(json.cosignatoryAccount.privateKey, NetworkType.MIJIN_TEST); + cosignAccount2 = Account.createFromPrivateKey(json.cosignatory2Account.privateKey, NetworkType.MIJIN_TEST); + cosignAccount3 = Account.createFromPrivateKey(json.cosignatory3Account.privateKey, NetworkType.MIJIN_TEST); + url = json.apiUrl; + generationHash = json.generationHash; + transactionHttp = new TransactionHttp(url); + namespaceHttp = new NamespaceHttp(url); + transactionService = new TransactionService(url); + done(); + }); + }); + + /** + * ========================= + * Setup test data + * ========================= + */ + describe('Get network currency mosaic id', () => { + it('get mosaicId', (done) => { + namespaceHttp.getLinkedMosaicId(new NamespaceId('cat.currency')).subscribe((networkMosaicId) => { + networkCurrencyMosaicId = networkMosaicId; + done(); + }); + }); + }); + + describe('Setup test multisig account', () => { + let listener: Listener; + before (() => { + listener = new Listener(config.apiUrl); + return listener.open(); + }); + after(() => { + return listener.close(); + }); + it('Announce MultisigAccountModificationTransaction', (done) => { + const modifyMultisigAccountTransaction = MultisigAccountModificationTransaction.create( + Deadline.create(), + 2, + 1, + [ + cosignAccount1.publicAccount, + cosignAccount2.publicAccount, + cosignAccount3.publicAccount, + ], + [], + NetworkType.MIJIN_TEST, + ); + + const aggregateTransaction = AggregateTransaction.createComplete(Deadline.create(), + [modifyMultisigAccountTransaction.toAggregate(multisigAccount.publicAccount)], + NetworkType.MIJIN_TEST, + []); + const signedTransaction = aggregateTransaction + .signTransactionWithCosignatories(multisigAccount, [cosignAccount1, cosignAccount2, cosignAccount3], generationHash); + + listener.confirmed(multisigAccount.address).subscribe(() => { + done(); + }); + listener.status(multisigAccount.address).subscribe((error) => { + console.log('Error:', error); + done(); + }); + transactionHttp.announce(signedTransaction); + }); + }); + + /** + * ========================= + * Test + * ========================= + */ + + describe('should announce transaction', () => { + let listener: Listener; + before (() => { + listener = new Listener(config.apiUrl); + return listener.open(); + }); + after(() => { + return listener.close(); + }); + it('announce', (done) => { + const transferTransaction = TransferTransaction.create( + Deadline.create(), + account2.address, + [ + NetworkCurrencyMosaic.createAbsolute(1), + ], + PlainMessage.create('test-message'), + NetworkType.MIJIN_TEST, + ); + const signedTransaction = transferTransaction.signWith(account, generationHash); + transactionService.announce(signedTransaction, listener).subscribe((tx: TransferTransaction) => { + expect(tx.signer!.publicKey).to.be.equal(account.publicKey); + expect((tx.recipientAddress as Address).equals(account2.address)).to.be.true; + expect(tx.message.payload).to.be.equal('test-message'); + done(); + }); + }); + }); + + describe('should announce aggregate bonded with hashlock', () => { + let listener: Listener; + before (() => { + listener = new Listener(config.apiUrl); + return listener.open(); + }); + after(() => { + return listener.close(); + }); + it('announce', (done) => { + const signedAggregatedTransaction = + TransactionUtils.createSignedAggregatedBondTransaction(multisigAccount, account, account2.address, generationHash); + const lockFundsTransaction = LockFundsTransaction.create( + Deadline.create(), + new Mosaic(networkCurrencyMosaicId, UInt64.fromUint(10 * Math.pow(10, NetworkCurrencyMosaic.DIVISIBILITY))), + UInt64.fromUint(1000), + signedAggregatedTransaction, + NetworkType.MIJIN_TEST, + ); + const signedLockFundsTransaction = lockFundsTransaction.signWith(account, generationHash); + transactionService + .announceHashLockAggregateBonded(signedLockFundsTransaction, signedAggregatedTransaction, listener).subscribe((tx) => { + expect(tx.signer!.publicKey).to.be.equal(account.publicKey); + expect(tx.type).to.be.equal(TransactionType.AGGREGATE_BONDED); + done(); + }); + }); + }); + + describe('should announce aggregate bonded transaction', () => { + let listener: Listener; + before (() => { + listener = new Listener(config.apiUrl); + return listener.open(); + }); + after(() => { + return listener.close(); + }); + it('announce', (done) => { + const signedAggregatedTransaction = + TransactionUtils.createSignedAggregatedBondTransaction(multisigAccount, account, account2.address, generationHash); + const lockFundsTransaction = LockFundsTransaction.create( + Deadline.create(), + new Mosaic(networkCurrencyMosaicId, UInt64.fromUint(10 * Math.pow(10, NetworkCurrencyMosaic.DIVISIBILITY))), + UInt64.fromUint(1000), + signedAggregatedTransaction, + NetworkType.MIJIN_TEST, + ); + const signedLockFundsTransaction = lockFundsTransaction.signWith(account, generationHash); + transactionService.announce(signedLockFundsTransaction, listener).subscribe(() => { + transactionService.announceAggregateBonded(signedAggregatedTransaction, listener).subscribe((tx) => { + expect(tx.signer!.publicKey).to.be.equal(account.publicKey); + expect(tx.type).to.be.equal(TransactionType.AGGREGATE_BONDED); + done(); + }); + }); + }); + }); + + /** + * ========================= + * House Keeping + * ========================= + */ + + describe('Restore test multisig Accounts', () => { + let listener: Listener; + before (() => { + listener = new Listener(config.apiUrl); + return listener.open(); + }); + after(() => { + return listener.close(); + }); + it('Announce MultisigAccountModificationTransaction', (done) => { + const removeCosigner1 = MultisigAccountModificationTransaction.create( + Deadline.create(), + -1, + 0, + [], + [ cosignAccount1.publicAccount, + ], + NetworkType.MIJIN_TEST, + ); + const removeCosigner2 = MultisigAccountModificationTransaction.create( + Deadline.create(), + 0, + 0, + [], + [ + cosignAccount2.publicAccount, + ], + NetworkType.MIJIN_TEST, + ); + + const removeCosigner3 = MultisigAccountModificationTransaction.create( + Deadline.create(), + -1, + -1, + [], + [ + cosignAccount3.publicAccount, + ], + NetworkType.MIJIN_TEST, + ); + + const aggregateTransaction = AggregateTransaction.createComplete(Deadline.create(), + [removeCosigner1.toAggregate(multisigAccount.publicAccount), + removeCosigner2.toAggregate(multisigAccount.publicAccount), + removeCosigner3.toAggregate(multisigAccount.publicAccount)], + NetworkType.MIJIN_TEST, + []); + const signedTransaction = aggregateTransaction + .signTransactionWithCosignatories(cosignAccount1, [cosignAccount2, cosignAccount3], generationHash); + + listener.confirmed(cosignAccount1.address).subscribe(() => { + done(); + }); + listener.status(cosignAccount1.address).subscribe((error) => { + console.log('Error:', error); + done(); + }); + transactionHttp.announce(signedTransaction); + }); + }); +}); diff --git a/src/core/utils/TransactionMapping.ts b/src/core/utils/TransactionMapping.ts index 64a387c4d4..b579fd0cb6 100644 --- a/src/core/utils/TransactionMapping.ts +++ b/src/core/utils/TransactionMapping.ts @@ -18,7 +18,6 @@ import { CreateTransactionFromDTO } from '../../infrastructure/transaction/Creat import { CreateTransactionFromPayload } from '../../infrastructure/transaction/CreateTransactionFromPayload'; import { InnerTransaction } from '../../model/transaction/InnerTransaction'; import { Transaction } from '../../model/transaction/Transaction'; -import { SignSchema } from '../crypto/SignSchema'; export class TransactionMapping { diff --git a/src/core/utils/UnresolvedMapping.ts b/src/core/utils/UnresolvedMapping.ts index a77c4d33f8..1a8de4f376 100644 --- a/src/core/utils/UnresolvedMapping.ts +++ b/src/core/utils/UnresolvedMapping.ts @@ -14,11 +14,11 @@ * limitations under the License. */ import { Address } from '../../model/account/Address'; +import { NetworkType } from '../../model/blockchain/NetworkType'; import { MosaicId } from '../../model/mosaic/MosaicId'; import { NamespaceId } from '../../model/namespace/NamespaceId'; import { Convert } from '../format/Convert'; import { RawAddress } from '../format/RawAddress'; -import { NetworkType } from "../../model/blockchain/NetworkType"; /** * @internal diff --git a/src/infrastructure/Listener.ts b/src/infrastructure/Listener.ts index 582ef69ed2..f1778b29ed 100644 --- a/src/infrastructure/Listener.ts +++ b/src/infrastructure/Listener.ts @@ -239,15 +239,18 @@ export class Listener { * it emits a new Transaction in the event stream. * * @param address address we listen when a transaction is in confirmed state + * @param transactionHash transactionHash for filtering multiple transactions * @return an observable stream of Transaction with state confirmed */ - public confirmed(address: Address): Observable { + public confirmed(address: Address, transactionHash?: string): Observable { this.subscribeTo(`confirmedAdded/${address.plain()}`); return this.messageSubject.asObservable().pipe( filter((_) => _.channelName === ListenerChannelName.confirmedAdded), filter((_) => _.message instanceof Transaction), map((_) => _.message as Transaction), - filter((_) => this.transactionFromAddress(_, address))); + filter((_) => this.transactionFromAddress(_, address)), + filter((_) => transactionHash === undefined || _.transactionInfo!.hash === transactionHash), + ); } /** @@ -289,15 +292,18 @@ export class Listener { * it emits a new {@link AggregateTransaction} in the event stream. * * @param address address we listen when a transaction with missing signatures state + * @param transactionHash transactionHash for filtering multiple transactions * @return an observable stream of AggregateTransaction with missing signatures state */ - public aggregateBondedAdded(address: Address): Observable { + public aggregateBondedAdded(address: Address, transactionHash?: string): Observable { this.subscribeTo(`partialAdded/${address.plain()}`); return this.messageSubject.asObservable().pipe( filter((_) => _.channelName === ListenerChannelName.aggregateBondedAdded), filter((_) => _.message instanceof AggregateTransaction), map((_) => _.message as AggregateTransaction), - filter((_) => this.transactionFromAddress(_, address))); + filter((_) => this.transactionFromAddress(_, address)), + filter((_) => transactionHash === undefined || _.transactionInfo!.hash === transactionHash), + ); } /** diff --git a/src/infrastructure/receipt/CreateReceiptFromDTO.ts b/src/infrastructure/receipt/CreateReceiptFromDTO.ts index ee336b4e70..8eff4c9c87 100644 --- a/src/infrastructure/receipt/CreateReceiptFromDTO.ts +++ b/src/infrastructure/receipt/CreateReceiptFromDTO.ts @@ -14,12 +14,10 @@ * limitations under the License. */ +import { UnresolvedMapping } from '../../core/utils/UnresolvedMapping'; import { Address } from '../../model/account/Address'; import {PublicAccount} from '../../model/account/PublicAccount'; import {MosaicId} from '../../model/mosaic/MosaicId'; -import { AddressAlias } from '../../model/namespace/AddressAlias'; -import { AliasType } from '../../model/namespace/AliasType'; -import { MosaicAlias } from '../../model/namespace/MosaicAlias'; import { NamespaceId } from '../../model/namespace/NamespaceId'; import { ArtifactExpiryReceipt } from '../../model/receipt/ArtifactExpiryReceipt'; import { BalanceChangeReceipt } from '../../model/receipt/BalanceChangeReceipt'; @@ -95,7 +93,7 @@ const createResolutionStatement = (statementDTO, resolutionType): ResolutionStat return new ResolutionStatement( ResolutionType.Address, UInt64.fromNumericString(statementDTO.height), - Address.createFromEncoded(statementDTO.unresolved), + extractUnresolvedAddress(statementDTO.unresolved), statementDTO.resolutionEntries.map((entry) => { return new ResolutionEntry(Address.createFromEncoded(entry.resolved), new ReceiptSource(entry.source.primaryId, entry.source.secondaryId)); @@ -105,7 +103,7 @@ const createResolutionStatement = (statementDTO, resolutionType): ResolutionStat return new ResolutionStatement( ResolutionType.Mosaic, UInt64.fromNumericString(statementDTO.height), - new MosaicId(statementDTO.unresolved), + UnresolvedMapping.toUnresolvedMosaic(statementDTO.unresolved), statementDTO.resolutionEntries.map((entry) => { return new ResolutionEntry(new MosaicId(entry.resolved), new ReceiptSource(entry.source.primaryId, entry.source.secondaryId)); @@ -197,6 +195,12 @@ const createInflationReceipt = (receiptDTO): Receipt => { ); }; +/** + * @internal + * @param receiptType receipt type + * @param id Artifact id + * @returns {MosaicId | NamespaceId} + */ const extractArtifactId = (receiptType: ReceiptType, id: string): MosaicId | NamespaceId => { switch (receiptType) { case ReceiptType.Mosaic_Expired: @@ -208,3 +212,21 @@ const extractArtifactId = (receiptType: ReceiptType, id: string): MosaicId | Nam throw new Error('Receipt type is not supported.'); } }; + +/** + * @interal + * @param unresolvedAddress unresolved address + * @returns {Address | NamespaceId} + */ +const extractUnresolvedAddress = (unresolvedAddress: any): Address | NamespaceId => { + if (typeof unresolvedAddress === 'string') { + return UnresolvedMapping.toUnresolvedAddress(unresolvedAddress); + } else if (typeof unresolvedAddress === 'object') { // Is JSON object + if (unresolvedAddress.hasOwnProperty('address')) { + return Address.createFromRawAddress(unresolvedAddress.address); + } else if (unresolvedAddress.hasOwnProperty('id')) { + return NamespaceId.createFromEncoded(unresolvedAddress.id); + } + } + throw new Error(`UnresolvedAddress: ${unresolvedAddress} type is not recognised`); +}; diff --git a/src/infrastructure/transaction/CreateTransactionFromDTO.ts b/src/infrastructure/transaction/CreateTransactionFromDTO.ts index ea6e1802c2..c7bada7ca1 100644 --- a/src/infrastructure/transaction/CreateTransactionFromDTO.ts +++ b/src/infrastructure/transaction/CreateTransactionFromDTO.ts @@ -175,7 +175,7 @@ const CreateStandaloneTransactionFromDTO = (transactionDTO, transactionInfo): Tr transactionDTO.version, Deadline.createFromDTO(transactionDTO.deadline), UInt64.fromNumericString(transactionDTO.maxFee || '0'), - new MosaicId(transactionDTO.mosaicId), + UnresolvedMapping.toUnresolvedMosaic(transactionDTO.mosaicId), transactionDTO.action, UInt64.fromNumericString(transactionDTO.delta), transactionDTO.signature, diff --git a/src/model/receipt/ResolutionStatement.ts b/src/model/receipt/ResolutionStatement.ts index 051156d063..141e4ebadf 100644 --- a/src/model/receipt/ResolutionStatement.ts +++ b/src/model/receipt/ResolutionStatement.ts @@ -15,8 +15,10 @@ */ import { sha3_256 } from 'js-sha3'; +import { UnresolvedMapping } from '../../core/utils/UnresolvedMapping'; import { GeneratorUtils } from '../../infrastructure/catbuffer/GeneratorUtils'; import { Address } from '../account/Address'; +import { NetworkType } from '../blockchain/NetworkType'; import { MosaicId } from '../mosaic/MosaicId'; import { NamespaceId } from '../namespace/NamespaceId'; import { UInt64 } from '../UInt64'; @@ -24,8 +26,6 @@ import { ReceiptType } from './ReceiptType'; import { ReceiptVersion } from './ReceiptVersion'; import { ResolutionEntry } from './ResolutionEntry'; import { ResolutionType } from './ResolutionType'; -import { NetworkType } from "../blockchain/NetworkType"; -import { UnresolvedMapping } from "../../core/utils/UnresolvedMapping"; /** * When a transaction includes an alias, a so called resolution statement reflects the resolved value for that block: @@ -84,6 +84,118 @@ export class ResolutionStatement { return hasher.hex().toUpperCase(); } + /** + * @internal + * Find resolution entry for given primaryId and secondaryId + * @param primaryId Primary id + * @param secondaryId Secondary id + * @returns {ResolutionEntry | undefined} + */ + public getResolutionEntryById(primaryId: number, secondaryId: number): ResolutionEntry | undefined { + /* + Primary id and secondary id do not specifically map to the exact transaction index on the same block. + The ids are just the order of the resolution reflecting on the order of transactions (ordered by index). + E.g 1 - Bob -> 1 random.token -> Alice + 2 - Carol -> 1 random.token > Denis + Based on above example, 2 transactions (index 0 & 1) are created on the same block, however, only 1 + resolution entry get generated for both. + */ + const resolvedPrimaryId = this.getMaxAvailablePrimaryId(primaryId); + + /* + If no primaryId found, it means there's no resolution entry available for the process. Invalid entry. + + e.g. Given: + Entries: [{P:2, S:0}, {P:5, S:6}] + Transaction: [Inx:1(0+1), AggInx:0] + It should return Entry: undefined + */ + if (resolvedPrimaryId === 0) { + return undefined; + } else if (primaryId > resolvedPrimaryId) { + /* + If the transaction index is greater than the overall most recent source primary id. + Use the most recent resolution entry (Max.PrimaryId + Max.SecondaryId) + + e.g. Given: + Entries: [{P:1, S:0}, {P:2, S:0}, {P:4, S:2}, {P:4, S:4} {P:7, S:6}] + Transaction: [Inx:5(4+1), AggInx:0] + It should return Entry: {P:4, S:4} + + e.g. Given: + Entries: [{P:1, S:0}, {P:2, S:0}, {P:4, S:2}, {P:4, S:4}, {P:7, S:6}] + Transaction: [Inx:3(2+1), AggInx:0] + It should return Entry: {P:2, S:0} + */ + return this.resolutionEntries + .find((entry) => entry.source.primaryId === resolvedPrimaryId && + entry.source.secondaryId === this.getMaxSecondaryIdByPrimaryId(resolvedPrimaryId)); + } + + // When transaction index matches a primaryId, get the most recent secondaryId (resolvedPrimaryId can only <= primaryId) + const resolvedSecondaryId = this.getMaxSecondaryIdByPrimaryIdAndSecondaryId(resolvedPrimaryId, secondaryId); + + /* + If no most recent secondaryId matched transaction index, find previous resolution entry (most recent). + This means the resolution entry for the specific inner transaction (inside Aggregate) / + was generated previously outside the aggregate. It should return the previous entry (previous primaryId) + + e.g. Given: + Entries: [{P:1, S:0}, {P:2, S:0}, {P:5, S:6}] + Transaction: [Inx:5(4+1), AggInx:3(2+1)] + It should return Entry: {P:2, S:0} + */ + if (resolvedSecondaryId === 0 && resolvedSecondaryId !== secondaryId) { + const lastPrimaryId = this.getMaxAvailablePrimaryId(resolvedPrimaryId - 1); + return this.resolutionEntries.find((entry) => entry.source.primaryId === lastPrimaryId && + entry.source.secondaryId === this.getMaxSecondaryIdByPrimaryId(lastPrimaryId)); + } + + /* + Found a matched resolution entry on both primaryId and secondaryId + + e.g. Given: + Entries: [{P:1, S:0}, {P:2, S:0}, {P:5, S:6}] + Transaction: [Inx:5(4+1), AggInx:6(2+1)] + It should return Entry: {P:5, S:6} + */ + return this.resolutionEntries + .find((entry) => entry.source.primaryId === resolvedPrimaryId && entry.source.secondaryId === resolvedSecondaryId); + } + + /** + * @internal + * Get max secondary id by a given primaryId + * @param primaryId Primary source id + * @returns {number} + */ + private getMaxSecondaryIdByPrimaryId(primaryId: number): number { + return Math.max(...this.resolutionEntries.filter((entry) => entry.source.primaryId === primaryId) + .map((filtered) => filtered.source.secondaryId)); + } + + /** + * Get most `recent` available secondary id by a given primaryId + * @param primaryId Primary source id + * @param secondaryId Secondary source id + * @returns {number} + */ + private getMaxSecondaryIdByPrimaryIdAndSecondaryId(primaryId: number, secondaryId: number): number { + return Math.max(...this.resolutionEntries.filter((entry) => entry.source.primaryId === primaryId) + .map((filtered) => secondaryId >= filtered.source.secondaryId ? filtered.source.secondaryId : 0)); + } + + /** + * @internal + * Get most `recent` primary source id by a given id (transaction index) as PrimaryId might not be the same as block transaction index. + * @param primaryId Primary source id + * @returns {number} + */ + private getMaxAvailablePrimaryId(primaryId: number): number { + return Math.max(...this.resolutionEntries + .map((entry) => primaryId >= entry.source.primaryId ? entry.source.primaryId : 0)); + } + /** * @internal * Generate buffer for unresulved diff --git a/src/model/receipt/Statement.ts b/src/model/receipt/Statement.ts index be18c4447f..4753c63498 100644 --- a/src/model/receipt/Statement.ts +++ b/src/model/receipt/Statement.ts @@ -14,7 +14,12 @@ * limitations under the License. */ +import { Address } from '../account/Address'; +import { Mosaic } from '../mosaic/Mosaic'; +import { MosaicId } from '../mosaic/MosaicId'; +import { NamespaceId } from '../namespace/NamespaceId'; import { ResolutionStatement } from './ResolutionStatement'; +import { ResolutionType } from './ResolutionType'; import { TransactionStatement } from './TransactionStatement'; export class Statement { @@ -39,4 +44,99 @@ export class Statement { */ public readonly mosaicResolutionStatements: ResolutionStatement[]) { } + + /** + * Resolve unresolvedAddress from statement + * @param unresolvedAddress Unresolved address + * @param height Block height + * @param transactionIndex Transaction index + * @param aggregateTransactionIndex Aggregate transaction index + * @returns {Address} + */ + public resolveAddress(unresolvedAddress: Address | NamespaceId, + height: string, + transactionIndex: number, + aggregateTransactionIndex: number = 0): Address { + return unresolvedAddress instanceof NamespaceId ? + this.getResolvedFromReceipt(ResolutionType.Address, unresolvedAddress as NamespaceId, + transactionIndex, height, aggregateTransactionIndex) as Address : + unresolvedAddress; + } + + /** + * Resolve unresolvedMosaicId from statement + * @param unresolvedMosaicId Unresolved mosaic id + * @param height Block height + * @param transactionIndex Transaction index + * @param aggregateTransactionIndex Aggregate transaction index + * @returns {MosaicId} + */ + public resolveMosaicId(unresolvedMosaicId: MosaicId | NamespaceId, + height: string, + transactionIndex: number, + aggregateTransactionIndex: number = 0): MosaicId { + return unresolvedMosaicId instanceof NamespaceId ? + this.getResolvedFromReceipt(ResolutionType.Mosaic, unresolvedMosaicId as NamespaceId, + transactionIndex, height, aggregateTransactionIndex) as MosaicId : + unresolvedMosaicId; + } + + /** + * Resolve unresolvedMosaic from statement + * @param unresolvedMosaic Unresolved mosaic + * @param height Block height + * @param transactionIndex Transaction index + * @param aggregateTransactionIndex Aggregate transaction index + * @returns {Mosaic} + */ + public resolveMosaic(unresolvedMosaic: Mosaic, + height: string, + transactionIndex: number, + aggregateTransactionIndex: number = 0): Mosaic { + return unresolvedMosaic.id instanceof NamespaceId ? + new Mosaic(this.getResolvedFromReceipt(ResolutionType.Mosaic, unresolvedMosaic.id as NamespaceId, + transactionIndex, height, aggregateTransactionIndex) as MosaicId, unresolvedMosaic.amount) : + unresolvedMosaic; + } + + /** + * @internal + * Extract resolved address | mosaic from block receipt + * @param resolutionType Resolution type: Address / Mosaic + * @param unresolved Unresolved address / mosaicId + * @param transactionIndex Transaction index + * @param height Transaction height + * @param aggregateTransactionIndex Transaction index for aggregate + * @returns {MosaicId | Address} + */ + private getResolvedFromReceipt(resolutionType: ResolutionType, + unresolved: NamespaceId, + transactionIndex: number, + height: string, + aggregateTransactionIndex?: number): MosaicId | Address { + + const resolutionStatement = (resolutionType === ResolutionType.Address ? this.addressResolutionStatements : + this.mosaicResolutionStatements).find((resolution) => resolution.height.toString() === height && + (resolution.unresolved as NamespaceId).equals(unresolved)); + + if (!resolutionStatement) { + throw new Error(`No resolution statement found on block: ${height} for unresolved: ${unresolved.toHex()}`); + } + + // If only one entry exists on the statement, just return + if (resolutionStatement.resolutionEntries.length === 1) { + return resolutionStatement.resolutionEntries[0].resolved; + } + + // Get the most recent resolution entry + const resolutionEntry = resolutionStatement.getResolutionEntryById( + aggregateTransactionIndex !== undefined ? aggregateTransactionIndex + 1 : transactionIndex + 1, + aggregateTransactionIndex !== undefined ? transactionIndex + 1 : 0, + ); + + if (!resolutionEntry) { + throw new Error(`No resolution entry found on block: ${height} for unresolved: ${unresolved.toHex()}`); + } + return resolutionEntry.resolved; + } } diff --git a/src/model/transaction/AccountAddressRestrictionTransaction.ts b/src/model/transaction/AccountAddressRestrictionTransaction.ts index 90a513ee1a..62d28e2988 100644 --- a/src/model/transaction/AccountAddressRestrictionTransaction.ts +++ b/src/model/transaction/AccountAddressRestrictionTransaction.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Convert, RawAddress } from '../../core/format'; +import { Convert } from '../../core/format'; import { UnresolvedMapping } from '../../core/utils/UnresolvedMapping'; import { AccountAddressRestrictionTransactionBuilder } from '../../infrastructure/catbuffer/AccountAddressRestrictionTransactionBuilder'; import { AmountDto } from '../../infrastructure/catbuffer/AmountDto'; @@ -29,9 +29,9 @@ import { Address } from '../account/Address'; import { PublicAccount } from '../account/PublicAccount'; import { NetworkType } from '../blockchain/NetworkType'; import { NamespaceId } from '../namespace/NamespaceId'; +import { Statement } from '../receipt/Statement'; import { AccountRestrictionFlags } from '../restriction/AccountRestrictionType'; import { UInt64 } from '../UInt64'; -import { AccountRestrictionModification } from './AccountRestrictionModification'; import { Deadline } from './Deadline'; import { InnerTransaction } from './InnerTransaction'; import { Transaction } from './Transaction'; @@ -190,4 +190,28 @@ export class AccountAddressRestrictionTransaction extends Transaction { ); return transactionBuilder.serialize(); } + + /** + * @internal + * @param statement Block receipt statement + * @param aggregateTransactionIndex Transaction index for aggregated transaction + * @returns {AccountAddressRestrictionTransaction} + */ + resolveAliases(statement: Statement, aggregateTransactionIndex: number = 0): AccountAddressRestrictionTransaction { + const transactionInfo = this.checkTransactionHeightAndIndex(); + return new AccountAddressRestrictionTransaction( + this.networkType, + this.version, + this.deadline, + this.maxFee, + this.restrictionFlags, + this.restrictionAdditions.map((addition) => statement.resolveAddress(addition, transactionInfo.height.toString(), + transactionInfo.index, aggregateTransactionIndex)), + this.restrictionDeletions.map((deletion) => statement.resolveAddress(deletion, transactionInfo.height.toString(), + transactionInfo.index, aggregateTransactionIndex)), + this.signature, + this.signer, + this.transactionInfo, + ); + } } diff --git a/src/model/transaction/AccountLinkTransaction.ts b/src/model/transaction/AccountLinkTransaction.ts index b5c2aa9308..9e6e813de3 100644 --- a/src/model/transaction/AccountLinkTransaction.ts +++ b/src/model/transaction/AccountLinkTransaction.ts @@ -163,4 +163,12 @@ export class AccountLinkTransaction extends Transaction { ); return transactionBuilder.serialize(); } + + /** + * @internal + * @returns {AccountLinkTransaction} + */ + resolveAliases(): AccountLinkTransaction { + return this; + } } diff --git a/src/model/transaction/AccountMetadataTransaction.ts b/src/model/transaction/AccountMetadataTransaction.ts index bbfc10c435..78b2009887 100644 --- a/src/model/transaction/AccountMetadataTransaction.ts +++ b/src/model/transaction/AccountMetadataTransaction.ts @@ -193,4 +193,12 @@ export class AccountMetadataTransaction extends Transaction { ); return transactionBuilder.serialize(); } + + /** + * @internal + * @returns {AccountMetadataTransaction} + */ + resolveAliases(): AccountMetadataTransaction { + return this; + } } diff --git a/src/model/transaction/AccountMosaicRestrictionTransaction.ts b/src/model/transaction/AccountMosaicRestrictionTransaction.ts index 20bba4d76b..c06ff8dfcb 100644 --- a/src/model/transaction/AccountMosaicRestrictionTransaction.ts +++ b/src/model/transaction/AccountMosaicRestrictionTransaction.ts @@ -29,6 +29,7 @@ import { PublicAccount } from '../account/PublicAccount'; import { NetworkType } from '../blockchain/NetworkType'; import { MosaicId } from '../mosaic/MosaicId'; import { NamespaceId } from '../namespace/NamespaceId'; +import { Statement } from '../receipt/Statement'; import { AccountRestrictionFlags } from '../restriction/AccountRestrictionType'; import { UInt64 } from '../UInt64'; import { Deadline } from './Deadline'; @@ -189,4 +190,28 @@ export class AccountMosaicRestrictionTransaction extends Transaction { ); return transactionBuilder.serialize(); } + + /** + * @internal + * @param statement Block receipt statement + * @param aggregateTransactionIndex Transaction index for aggregated transaction + * @returns {AccountMosaicRestrictionTransaction} + */ + resolveAliases(statement: Statement, aggregateTransactionIndex: number = 0): AccountMosaicRestrictionTransaction { + const transactionInfo = this.checkTransactionHeightAndIndex(); + return new AccountMosaicRestrictionTransaction( + this.networkType, + this.version, + this.deadline, + this.maxFee, + this.restrictionFlags, + this.restrictionAdditions.map((addition) => statement.resolveMosaicId(addition, transactionInfo.height.toString(), + transactionInfo.index, aggregateTransactionIndex)), + this.restrictionDeletions.map((deletion) => statement.resolveMosaicId(deletion, transactionInfo.height.toString(), + transactionInfo.index, aggregateTransactionIndex)), + this.signature, + this.signer, + this.transactionInfo, + ); + } } diff --git a/src/model/transaction/AccountOperationRestrictionTransaction.ts b/src/model/transaction/AccountOperationRestrictionTransaction.ts index 53661b6db4..899602794f 100644 --- a/src/model/transaction/AccountOperationRestrictionTransaction.ts +++ b/src/model/transaction/AccountOperationRestrictionTransaction.ts @@ -174,4 +174,12 @@ export class AccountOperationRestrictionTransaction extends Transaction { ); return transactionBuilder.serialize(); } + + /** + * @internal + * @returns {AccountOperationRestrictionTransaction} + */ + resolveAliases(): AccountOperationRestrictionTransaction { + return this; + } } diff --git a/src/model/transaction/AddressAliasTransaction.ts b/src/model/transaction/AddressAliasTransaction.ts index 14b18f2c16..8f58f5d2a4 100644 --- a/src/model/transaction/AddressAliasTransaction.ts +++ b/src/model/transaction/AddressAliasTransaction.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { Observable } from 'rxjs/internal/Observable'; +import { of } from 'rxjs/internal/observable/of'; import { Convert, RawAddress } from '../../core/format'; import { AddressAliasTransactionBuilder } from '../../infrastructure/catbuffer/AddressAliasTransactionBuilder'; import { AddressDto } from '../../infrastructure/catbuffer/AddressDto'; @@ -23,6 +25,7 @@ import { KeyDto } from '../../infrastructure/catbuffer/KeyDto'; import { NamespaceIdDto } from '../../infrastructure/catbuffer/NamespaceIdDto'; import { SignatureDto } from '../../infrastructure/catbuffer/SignatureDto'; import { TimestampDto } from '../../infrastructure/catbuffer/TimestampDto'; +import { ReceiptHttp } from '../../infrastructure/ReceiptHttp'; import { Address } from '../account/Address'; import { PublicAccount } from '../account/PublicAccount'; import { NetworkType } from '../blockchain/NetworkType'; @@ -183,4 +186,12 @@ export class AddressAliasTransaction extends Transaction { ); return transactionBuilder.serialize(); } + + /** + * @internal + * @returns {AddressAliasTransaction} + */ + resolveAliases(): AddressAliasTransaction { + return this; + } } diff --git a/src/model/transaction/AggregateTransaction.ts b/src/model/transaction/AggregateTransaction.ts index a7ea36d201..250dd324b5 100644 --- a/src/model/transaction/AggregateTransaction.ts +++ b/src/model/transaction/AggregateTransaction.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { sha3_256 } from 'js-sha3'; import {KeyPair, MerkleHashBuilder, SHA3Hasher, SignSchema} from '../../core/crypto'; -import {Convert, RawArray} from '../../core/format'; +import {Convert} from '../../core/format'; import {AggregateBondedTransactionBuilder} from '../../infrastructure/catbuffer/AggregateBondedTransactionBuilder'; import {AggregateCompleteTransactionBuilder} from '../../infrastructure/catbuffer/AggregateCompleteTransactionBuilder'; import {AmountDto} from '../../infrastructure/catbuffer/AmountDto'; @@ -30,6 +29,7 @@ import {CreateTransactionFromPayload} from '../../infrastructure/transaction/Cre import {Account} from '../account/Account'; import {PublicAccount} from '../account/PublicAccount'; import {NetworkType} from '../blockchain/NetworkType'; +import { Statement } from '../receipt/Statement'; import {UInt64} from '../UInt64'; import {AggregateTransactionCosignature} from './AggregateTransactionCosignature'; import {CosignatureSignedTransaction} from './CosignatureSignedTransaction'; @@ -400,4 +400,25 @@ export class AggregateTransaction extends Transaction { private getInnerTransactionPaddingSize(size: number, alignment: number): number { return 0 === size % alignment ? 0 : alignment - (size % alignment); } + + /** + * @internal + * @returns {AggregateTransaction} + */ + resolveAliases(statement: Statement): AggregateTransaction { + const transactionInfo = this.checkTransactionHeightAndIndex(); + return new AggregateTransaction( + this.networkType, + this.type, + this.version, + this.deadline, + this.maxFee, + this.innerTransactions.map((tx) => tx.resolveAliases(statement, transactionInfo.index)) + .sort((a, b) => a.transactionInfo!.index - b.transactionInfo!.index), + this.cosignatures, + this.signature, + this.signer, + this.transactionInfo, + ); + } } diff --git a/src/model/transaction/LockFundsTransaction.ts b/src/model/transaction/LockFundsTransaction.ts index a772bd3e6e..666c0cceb8 100644 --- a/src/model/transaction/LockFundsTransaction.ts +++ b/src/model/transaction/LockFundsTransaction.ts @@ -29,6 +29,7 @@ import { PublicAccount } from '../account/PublicAccount'; import { NetworkType } from '../blockchain/NetworkType'; import { Mosaic } from '../mosaic/Mosaic'; import { MosaicId } from '../mosaic/MosaicId'; +import { Statement } from '../receipt/Statement'; import { UInt64 } from '../UInt64'; import { Deadline } from './Deadline'; import { InnerTransaction } from './InnerTransaction'; @@ -50,6 +51,7 @@ export class LockFundsTransaction extends Transaction { * Aggregate bonded hash. */ public readonly hash: string; + signedTransaction: SignedTransaction; /** * Create a Lock funds transaction object @@ -108,6 +110,7 @@ export class LockFundsTransaction extends Transaction { transactionInfo?: TransactionInfo) { super(TransactionType.LOCK, networkType, version, deadline, maxFee, signature, signer, transactionInfo); this.hash = signedTransaction.hash; + this.signedTransaction = signedTransaction; if (signedTransaction.type !== TransactionType.AGGREGATE_BONDED) { throw new Error('Signed transaction must be Aggregate Bonded Transaction'); } @@ -200,4 +203,27 @@ export class LockFundsTransaction extends Transaction { ); return transactionBuilder.serialize(); } + + /** + * @internal + * @param statement Block receipt statement + * @param aggregateTransactionIndex Transaction index for aggregated transaction + * @returns {LockFundsTransaction} + */ + resolveAliases(statement: Statement, aggregateTransactionIndex: number = 0): LockFundsTransaction { + const transactionInfo = this.checkTransactionHeightAndIndex(); + return new LockFundsTransaction( + this.networkType, + this.version, + this.deadline, + this.maxFee, + statement.resolveMosaic(this.mosaic, transactionInfo.height.toString(), + transactionInfo.index, aggregateTransactionIndex), + this.duration, + this.signedTransaction, + this.signature, + this.signer, + this.transactionInfo, + ); + } } diff --git a/src/model/transaction/MosaicAddressRestrictionTransaction.ts b/src/model/transaction/MosaicAddressRestrictionTransaction.ts index 11b25d3855..b6b513ba79 100644 --- a/src/model/transaction/MosaicAddressRestrictionTransaction.ts +++ b/src/model/transaction/MosaicAddressRestrictionTransaction.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Convert, RawAddress } from '../../core/format'; +import { Convert } from '../../core/format'; import { UnresolvedMapping } from '../../core/utils/UnresolvedMapping'; import { AmountDto } from '../../infrastructure/catbuffer/AmountDto'; import { @@ -31,6 +31,7 @@ import { PublicAccount } from '../account/PublicAccount'; import { NetworkType } from '../blockchain/NetworkType'; import { MosaicId } from '../mosaic/MosaicId'; import { NamespaceId } from '../namespace/NamespaceId'; +import { Statement } from '../receipt/Statement'; import { UInt64 } from '../UInt64'; import { Deadline } from './Deadline'; import { InnerTransaction } from './InnerTransaction'; @@ -233,4 +234,30 @@ export class MosaicAddressRestrictionTransaction extends Transaction { ); return transactionBuilder.serialize(); } + + /** + * @internal + * @param statement Block receipt statement + * @param aggregateTransactionIndex Transaction index for aggregated transaction + * @returns {MosaicAddressRestrictionTransaction} + */ + resolveAliases(statement: Statement, aggregateTransactionIndex: number = 0): MosaicAddressRestrictionTransaction { + const transactionInfo = this.checkTransactionHeightAndIndex(); + return new MosaicAddressRestrictionTransaction( + this.networkType, + this.version, + this.deadline, + this.maxFee, + statement.resolveMosaicId(this.mosaicId, transactionInfo.height.toString(), + transactionInfo.index, aggregateTransactionIndex), + this.restrictionKey, + statement.resolveAddress(this.targetAddress, + transactionInfo.height.toString(), transactionInfo.index, aggregateTransactionIndex), + this.previousRestrictionValue, + this.newRestrictionValue, + this.signature, + this.signer, + this.transactionInfo, + ); + } } diff --git a/src/model/transaction/MosaicAliasTransaction.ts b/src/model/transaction/MosaicAliasTransaction.ts index 4fd81443ce..f259c5af13 100644 --- a/src/model/transaction/MosaicAliasTransaction.ts +++ b/src/model/transaction/MosaicAliasTransaction.ts @@ -178,4 +178,12 @@ export class MosaicAliasTransaction extends Transaction { ); return transactionBuilder.serialize(); } + + /** + * @internal + * @returns {MosaicAliasTransaction} + */ + resolveAliases(): MosaicAliasTransaction { + return this; + } } diff --git a/src/model/transaction/MosaicDefinitionTransaction.ts b/src/model/transaction/MosaicDefinitionTransaction.ts index 179c37e0e7..a43a733825 100644 --- a/src/model/transaction/MosaicDefinitionTransaction.ts +++ b/src/model/transaction/MosaicDefinitionTransaction.ts @@ -221,4 +221,12 @@ export class MosaicDefinitionTransaction extends Transaction { ); return transactionBuilder.serialize(); } + + /** + * @internal + * @returns {MosaicDefinitionTransaction} + */ + resolveAliases(): MosaicDefinitionTransaction { + return this; + } } diff --git a/src/model/transaction/MosaicGlobalRestrictionTransaction.ts b/src/model/transaction/MosaicGlobalRestrictionTransaction.ts index 404f14e63c..5b6853092c 100644 --- a/src/model/transaction/MosaicGlobalRestrictionTransaction.ts +++ b/src/model/transaction/MosaicGlobalRestrictionTransaction.ts @@ -29,6 +29,7 @@ import { PublicAccount } from '../account/PublicAccount'; import { NetworkType } from '../blockchain/NetworkType'; import { MosaicId } from '../mosaic/MosaicId'; import { NamespaceId } from '../namespace/NamespaceId'; +import { Statement } from '../receipt/Statement'; import { MosaicRestrictionType } from '../restriction/MosaicRestrictionType'; import { UInt64 } from '../UInt64'; import { Deadline } from './Deadline'; @@ -243,4 +244,32 @@ export class MosaicGlobalRestrictionTransaction extends Transaction { ); return transactionBuilder.serialize(); } + + /** + * @internal + * @param statement Block receipt statement + * @param aggregateTransactionIndex Transaction index for aggregated transaction + * @returns {MosaicGlobalRestrictionTransaction} + */ + resolveAliases(statement: Statement, aggregateTransactionIndex: number = 0): MosaicGlobalRestrictionTransaction { + const transactionInfo = this.checkTransactionHeightAndIndex(); + return new MosaicGlobalRestrictionTransaction( + this.networkType, + this.version, + this.deadline, + this.maxFee, + statement.resolveMosaicId(this.mosaicId, transactionInfo.height.toString(), + transactionInfo.index, aggregateTransactionIndex), + statement.resolveMosaicId(this.referenceMosaicId, transactionInfo.height.toString(), + transactionInfo.index, aggregateTransactionIndex), + this.restrictionKey, + this.previousRestrictionValue, + this.previousRestrictionType, + this.newRestrictionValue, + this.newRestrictionType, + this.signature, + this.signer, + this.transactionInfo, + ); + } } diff --git a/src/model/transaction/MosaicMetadataTransaction.ts b/src/model/transaction/MosaicMetadataTransaction.ts index 6973af8283..01ad53bc1d 100644 --- a/src/model/transaction/MosaicMetadataTransaction.ts +++ b/src/model/transaction/MosaicMetadataTransaction.ts @@ -27,6 +27,7 @@ import { PublicAccount } from '../account/PublicAccount'; import { NetworkType } from '../blockchain/NetworkType'; import { MosaicId } from '../mosaic/MosaicId'; import { NamespaceId } from '../namespace/NamespaceId'; +import { Statement } from '../receipt/Statement'; import { UInt64 } from '../UInt64'; import { Deadline } from './Deadline'; import { InnerTransaction } from './InnerTransaction'; @@ -209,4 +210,29 @@ export class MosaicMetadataTransaction extends Transaction { ); return transactionBuilder.serialize(); } + + /** + * @internal + * @param statement Block receipt statement + * @param aggregateTransactionIndex Transaction index for aggregated transaction + * @returns {MosaicMetadataTransaction} + */ + resolveAliases(statement: Statement, aggregateTransactionIndex: number = 0): MosaicMetadataTransaction { + const transactionInfo = this.checkTransactionHeightAndIndex(); + return new MosaicMetadataTransaction( + this.networkType, + this.version, + this.deadline, + this.maxFee, + this.targetPublicKey, + this.scopedMetadataKey, + statement.resolveMosaicId(this.targetMosaicId, transactionInfo.height.toString(), + transactionInfo.index, aggregateTransactionIndex), + this.valueSizeDelta, + this.value, + this.signature, + this.signer, + this.transactionInfo, + ); + } } diff --git a/src/model/transaction/MosaicSupplyChangeTransaction.ts b/src/model/transaction/MosaicSupplyChangeTransaction.ts index 5a3fea7dd5..c60894309c 100644 --- a/src/model/transaction/MosaicSupplyChangeTransaction.ts +++ b/src/model/transaction/MosaicSupplyChangeTransaction.ts @@ -15,6 +15,7 @@ */ import { Convert } from '../../core/format'; +import { UnresolvedMapping } from '../../core/utils/UnresolvedMapping'; import { AmountDto } from '../../infrastructure/catbuffer/AmountDto'; import { EmbeddedMosaicSupplyChangeTransactionBuilder } from '../../infrastructure/catbuffer/EmbeddedMosaicSupplyChangeTransactionBuilder'; import { KeyDto } from '../../infrastructure/catbuffer/KeyDto'; @@ -26,6 +27,8 @@ import { PublicAccount } from '../account/PublicAccount'; import { NetworkType } from '../blockchain/NetworkType'; import { MosaicId } from '../mosaic/MosaicId'; import { MosaicSupplyChangeAction } from '../mosaic/MosaicSupplyChangeAction'; +import { NamespaceId } from '../namespace/NamespaceId'; +import { Statement } from '../receipt/Statement'; import { UInt64 } from '../UInt64'; import { Deadline } from './Deadline'; import { InnerTransaction } from './InnerTransaction'; @@ -43,7 +46,7 @@ export class MosaicSupplyChangeTransaction extends Transaction { /** * Create a mosaic supply change transaction object * @param deadline - The deadline to include the transaction. - * @param mosaicId - The mosaic id. + * @param mosaicId - The unresolved mosaic id. * @param action - The supply change action (increase | decrease). * @param delta - The supply change in units for the mosaic. * @param networkType - The network type. @@ -51,7 +54,7 @@ export class MosaicSupplyChangeTransaction extends Transaction { * @returns {MosaicSupplyChangeTransaction} */ public static create(deadline: Deadline, - mosaicId: MosaicId, + mosaicId: MosaicId | NamespaceId, action: MosaicSupplyChangeAction, delta: UInt64, networkType: NetworkType, @@ -83,9 +86,9 @@ export class MosaicSupplyChangeTransaction extends Transaction { deadline: Deadline, maxFee: UInt64, /** - * The mosaic id. + * The unresolved mosaic id. */ - public readonly mosaicId: MosaicId, + public readonly mosaicId: MosaicId | NamespaceId, /** * The supply type. */ @@ -115,7 +118,7 @@ export class MosaicSupplyChangeTransaction extends Transaction { const transaction = MosaicSupplyChangeTransaction.create( isEmbedded ? Deadline.create() : Deadline.createFromDTO( (builder as MosaicSupplyChangeTransactionBuilder).getDeadline().timestamp), - new MosaicId(builder.getMosaicId().unresolvedMosaicId), + UnresolvedMapping.toUnresolvedMosaic(new UInt64(builder.getMosaicId().unresolvedMosaicId).toHex()), builder.getAction().valueOf(), new UInt64(builder.getDelta().amount), networkType, @@ -181,4 +184,27 @@ export class MosaicSupplyChangeTransaction extends Transaction { ); return transactionBuilder.serialize(); } + + /** + * @internal + * @param statement Block receipt statement + * @param aggregateTransactionIndex Transaction index for aggregated transaction + * @returns {MosaicSupplyChangeTransaction} + */ + resolveAliases(statement: Statement, aggregateTransactionIndex: number = 0): MosaicSupplyChangeTransaction { + const transactionInfo = this.checkTransactionHeightAndIndex(); + return new MosaicSupplyChangeTransaction( + this.networkType, + this.version, + this.deadline, + this.maxFee, + statement.resolveMosaicId(this.mosaicId, transactionInfo.height.toString(), + transactionInfo.index, aggregateTransactionIndex), + this.action, + this.delta, + this.signature, + this.signer, + this.transactionInfo, + ); + } } diff --git a/src/model/transaction/MultisigAccountModificationTransaction.ts b/src/model/transaction/MultisigAccountModificationTransaction.ts index c2ecd6c9c8..d8a4dc1d77 100644 --- a/src/model/transaction/MultisigAccountModificationTransaction.ts +++ b/src/model/transaction/MultisigAccountModificationTransaction.ts @@ -29,7 +29,6 @@ import { NetworkType } from '../blockchain/NetworkType'; import { UInt64 } from '../UInt64'; import { Deadline } from './Deadline'; import { InnerTransaction } from './InnerTransaction'; -import { MultisigCosignatoryModification } from './MultisigCosignatoryModification'; import { Transaction } from './Transaction'; import { TransactionInfo } from './TransactionInfo'; import { TransactionType } from './TransactionType'; @@ -213,4 +212,12 @@ export class MultisigAccountModificationTransaction extends Transaction { ); return transactionBuilder.serialize(); } + + /** + * @internal + * @returns {MultisigAccountModificationTransaction} + */ + resolveAliases(): MultisigAccountModificationTransaction { + return this; + } } diff --git a/src/model/transaction/NamespaceMetadataTransaction.ts b/src/model/transaction/NamespaceMetadataTransaction.ts index b5dd3f8d11..f58453c268 100644 --- a/src/model/transaction/NamespaceMetadataTransaction.ts +++ b/src/model/transaction/NamespaceMetadataTransaction.ts @@ -208,4 +208,12 @@ export class NamespaceMetadataTransaction extends Transaction { ); return transactionBuilder.serialize(); } + + /** + * @internal + * @returns {NamespaceMetadataTransaction} + */ + resolveAliases(): NamespaceMetadataTransaction { + return this; + } } diff --git a/src/model/transaction/NamespaceRegistrationTransaction.ts b/src/model/transaction/NamespaceRegistrationTransaction.ts index b10a46714d..935ee02bc7 100644 --- a/src/model/transaction/NamespaceRegistrationTransaction.ts +++ b/src/model/transaction/NamespaceRegistrationTransaction.ts @@ -273,4 +273,12 @@ export class NamespaceRegistrationTransaction extends Transaction { } return transactionBuilder.serialize(); } + + /** + * @internal + * @returns {NamespaceRegistrationTransaction} + */ + resolveAliases(): NamespaceRegistrationTransaction { + return this; + } } diff --git a/src/model/transaction/SecretLockTransaction.ts b/src/model/transaction/SecretLockTransaction.ts index dfb1b9f41a..97ed1d75b3 100644 --- a/src/model/transaction/SecretLockTransaction.ts +++ b/src/model/transaction/SecretLockTransaction.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Convert, Convert as convert, RawAddress } from '../../core/format'; +import { Convert, Convert as convert } from '../../core/format'; import { UnresolvedMapping } from '../../core/utils/UnresolvedMapping'; import { AmountDto } from '../../infrastructure/catbuffer/AmountDto'; import { BlockDurationDto } from '../../infrastructure/catbuffer/BlockDurationDto'; @@ -30,8 +30,8 @@ import { Address } from '../account/Address'; import { PublicAccount } from '../account/PublicAccount'; import { NetworkType } from '../blockchain/NetworkType'; import { Mosaic } from '../mosaic/Mosaic'; -import { MosaicId } from '../mosaic/MosaicId'; import { NamespaceId } from '../namespace/NamespaceId'; +import { Statement } from '../receipt/Statement'; import { UInt64 } from '../UInt64'; import { Deadline } from './Deadline'; import { HashType, HashTypeLengthValidator } from './HashType'; @@ -231,4 +231,30 @@ export class SecretLockTransaction extends Transaction { ); return transactionBuilder.serialize(); } + + /** + * @internal + * @param statement Block receipt statement + * @param aggregateTransactionIndex Transaction index for aggregated transaction + * @returns {SecretLockTransaction} + */ + resolveAliases(statement: Statement, aggregateTransactionIndex: number = 0): SecretLockTransaction { + const transactionInfo = this.checkTransactionHeightAndIndex(); + return new SecretLockTransaction( + this.networkType, + this.version, + this.deadline, + this.maxFee, + statement.resolveMosaic(this.mosaic, transactionInfo.height.toString(), + transactionInfo.index, aggregateTransactionIndex), + this.duration, + this.hashType, + this.secret, + statement.resolveAddress(this.recipientAddress, + transactionInfo.height.toString(), transactionInfo.index, aggregateTransactionIndex), + this.signature, + this.signer, + this.transactionInfo, + ); + } } diff --git a/src/model/transaction/SecretProofTransaction.ts b/src/model/transaction/SecretProofTransaction.ts index 27a5d868e3..1ae4d90e1b 100644 --- a/src/model/transaction/SecretProofTransaction.ts +++ b/src/model/transaction/SecretProofTransaction.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { Convert, Convert as convert, RawAddress } from '../../core/format'; +import { Observable } from 'rxjs/internal/Observable'; +import { of } from 'rxjs/internal/observable/of'; +import { map } from 'rxjs/operators'; +import { Convert, Convert as convert } from '../../core/format'; import { UnresolvedMapping } from '../../core/utils/UnresolvedMapping'; import { AmountDto } from '../../infrastructure/catbuffer/AmountDto'; import { EmbeddedSecretProofTransactionBuilder } from '../../infrastructure/catbuffer/EmbeddedSecretProofTransactionBuilder'; @@ -24,10 +27,14 @@ import { SecretProofTransactionBuilder } from '../../infrastructure/catbuffer/Se import { SignatureDto } from '../../infrastructure/catbuffer/SignatureDto'; import { TimestampDto } from '../../infrastructure/catbuffer/TimestampDto'; import { UnresolvedAddressDto } from '../../infrastructure/catbuffer/UnresolvedAddressDto'; +import { ReceiptHttp } from '../../infrastructure/ReceiptHttp'; +import { TransactionService } from '../../service/TransactionService'; import { Address } from '../account/Address'; import { PublicAccount } from '../account/PublicAccount'; import { NetworkType } from '../blockchain/NetworkType'; import { NamespaceId } from '../namespace/NamespaceId'; +import { ResolutionType } from '../receipt/ResolutionType'; +import { Statement } from '../receipt/Statement'; import { UInt64 } from '../UInt64'; import { Deadline } from './Deadline'; import { HashType, HashTypeLengthValidator } from './HashType'; @@ -207,4 +214,28 @@ export class SecretProofTransaction extends Transaction { ); return transactionBuilder.serialize(); } + + /** + * @internal + * @param statement Block receipt statement + * @param aggregateTransactionIndex Transaction index for aggregated transaction + * @returns {SecretProofTransaction} + */ + resolveAliases(statement: Statement, aggregateTransactionIndex: number = 0): SecretProofTransaction { + const transactionInfo = this.checkTransactionHeightAndIndex(); + return new SecretProofTransaction( + this.networkType, + this.version, + this.deadline, + this.maxFee, + this.hashType, + this.secret, + statement.resolveAddress(this.recipientAddress, + transactionInfo.height.toString(), transactionInfo.index, aggregateTransactionIndex), + this.proof, + this.signature, + this.signer, + this.transactionInfo, + ); + } } diff --git a/src/model/transaction/SignedTransaction.ts b/src/model/transaction/SignedTransaction.ts index addc04c6a1..bc776b4eb6 100644 --- a/src/model/transaction/SignedTransaction.ts +++ b/src/model/transaction/SignedTransaction.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { Address } from '../account/Address'; +import { PublicAccount } from '../account/PublicAccount'; import {NetworkType} from '../blockchain/NetworkType'; /** @@ -65,4 +67,12 @@ export class SignedTransaction { networkType: this.networkType, }; } + + /** + * Return signer's address + * @returns {Address} + */ + getSignerAddress(): Address { + return PublicAccount.createFromPublicKey(this.signerPublicKey, this.networkType).address; + } } diff --git a/src/model/transaction/Transaction.ts b/src/model/transaction/Transaction.ts index f051d6ceeb..7fc41776c6 100644 --- a/src/model/transaction/Transaction.ts +++ b/src/model/transaction/Transaction.ts @@ -20,6 +20,7 @@ import { SerializeTransactionToJSON } from '../../infrastructure/transaction/Ser import { Account } from '../account/Account'; import { PublicAccount } from '../account/PublicAccount'; import { NetworkType } from '../blockchain/NetworkType'; +import { Statement } from '../receipt/Statement'; import { UInt64 } from '../UInt64'; import { AggregateTransactionInfo } from './AggregateTransactionInfo'; import { Deadline } from './Deadline'; @@ -185,6 +186,14 @@ export abstract class Transaction { */ protected abstract generateEmbeddedBytes(): Uint8Array; + /** + * @internal + * @param statement Block receipt statement + * @param AggregateTransactionIndex Transaction index for aggregated transaction + * @returns {Observable} + */ + abstract resolveAliases(statement?: Statement, aggregateTransactionIndex?: number): Transaction; + /** * @internal * Serialize and sign transaction creating a new SignedTransaction @@ -383,4 +392,18 @@ export abstract class Transaction { const childClassObject = SerializeTransactionToJSON(this); return {transaction: Object.assign(commonTransactionObject, childClassObject)}; } + + /** + * @internal + * Check if index and height exists in transactionInfo + * @returns TransactionInfo + */ + protected checkTransactionHeightAndIndex(): TransactionInfo { + if (this.transactionInfo === undefined || + this.transactionInfo.height === undefined || + this.transactionInfo.index === undefined) { + throw new Error('Transaction height or index undefined'); + } + return this.transactionInfo; + } } diff --git a/src/model/transaction/TransferTransaction.ts b/src/model/transaction/TransferTransaction.ts index 12c91e2241..48e449b60a 100644 --- a/src/model/transaction/TransferTransaction.ts +++ b/src/model/transaction/TransferTransaction.ts @@ -15,7 +15,7 @@ */ import * as Long from 'long'; -import {Convert, Convert as convert} from '../../core/format'; +import {Convert} from '../../core/format'; import {UnresolvedMapping} from '../../core/utils/UnresolvedMapping'; import {AmountDto} from '../../infrastructure/catbuffer/AmountDto'; import {EmbeddedTransferTransactionBuilder} from '../../infrastructure/catbuffer/EmbeddedTransferTransactionBuilder'; @@ -36,6 +36,7 @@ import {MessageType} from '../message/MessageType'; import {PlainMessage} from '../message/PlainMessage'; import {Mosaic} from '../mosaic/Mosaic'; import {NamespaceId} from '../namespace/NamespaceId'; +import { Statement } from '../receipt/Statement'; import {UInt64} from '../UInt64'; import {Deadline} from './Deadline'; import {InnerTransaction} from './InnerTransaction'; @@ -272,4 +273,29 @@ export class TransferTransaction extends Transaction { ); return transactionBuilder.serialize(); } + + /** + * @internal + * @param statement Block receipt statement + * @param aggregateTransactionIndex Transaction index for aggregated transaction + * @returns {TransferTransaction} + */ + resolveAliases(statement: Statement, aggregateTransactionIndex: number = 0): TransferTransaction { + const transactionInfo = this.checkTransactionHeightAndIndex(); + return new TransferTransaction( + this.networkType, + this.version, + this.deadline, + this.maxFee, + statement.resolveAddress(this.recipientAddress, + transactionInfo.height.toString(), transactionInfo.index, aggregateTransactionIndex), + this.mosaics.map((mosaic) => + statement.resolveMosaic(mosaic, transactionInfo.height.toString(), + transactionInfo.index, aggregateTransactionIndex)), + this.message, + this.signature, + this.signer, + this.transactionInfo, + ); + } } diff --git a/src/service/TransactionService.ts b/src/service/TransactionService.ts new file mode 100644 index 0000000000..d0f54ba621 --- /dev/null +++ b/src/service/TransactionService.ts @@ -0,0 +1,190 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Observable, of} from 'rxjs'; +import { flatMap, map, mergeMap, toArray} from 'rxjs/operators'; +import { Listener } from '../infrastructure/Listener'; +import { ReceiptHttp } from '../infrastructure/ReceiptHttp'; +import { TransactionHttp } from '../infrastructure/TransactionHttp'; +import { NamespaceId } from '../model/namespace/NamespaceId'; +import { AccountAddressRestrictionTransaction } from '../model/transaction/AccountAddressRestrictionTransaction'; +import { AggregateTransaction } from '../model/transaction/AggregateTransaction'; +import { LockFundsTransaction } from '../model/transaction/LockFundsTransaction'; +import { MosaicAddressRestrictionTransaction } from '../model/transaction/MosaicAddressRestrictionTransaction'; +import { MosaicGlobalRestrictionTransaction } from '../model/transaction/MosaicGlobalRestrictionTransaction'; +import { MosaicMetadataTransaction } from '../model/transaction/MosaicMetadataTransaction'; +import { MosaicSupplyChangeTransaction } from '../model/transaction/MosaicSupplyChangeTransaction'; +import { SecretLockTransaction } from '../model/transaction/SecretLockTransaction'; +import { SecretProofTransaction } from '../model/transaction/SecretProofTransaction'; +import { SignedTransaction } from '../model/transaction/SignedTransaction'; +import { Transaction } from '../model/transaction/Transaction'; +import { TransactionType } from '../model/transaction/TransactionType'; +import { TransferTransaction } from '../model/transaction/TransferTransaction'; +import { ITransactionService } from './interfaces/ITransactionService'; + +/** + * Transaction Service + */ +export class TransactionService implements ITransactionService { + + private readonly transactionHttp: TransactionHttp; + private readonly receiptHttp: ReceiptHttp; + /** + * Constructor + * @param url Base catapult-rest url + */ + constructor(url: string) { + this.transactionHttp = new TransactionHttp(url); + this.receiptHttp = new ReceiptHttp(url); + } + + /** + * Resolve unresolved mosaic / address from array of transactions + * @param transationHashes List of transaction hashes. + * @param listener Websocket listener + * @returns Observable + */ + public resolveAliases(transationHashes: string[]): Observable { + return this.transactionHttp.getTransactions(transationHashes).pipe( + mergeMap((_) => _), + mergeMap((transaction) => this.resolveTransaction(transaction)), + toArray(), + ); + } + + /** + * @param signedTransaction Signed transaction to be announced. + * @param listener Websocket listener + * @returns {Observable} + */ + public announce(signedTransaction: SignedTransaction, listener: Listener): Observable { + return this.transactionHttp.announce(signedTransaction).pipe( + flatMap(() => listener.confirmed(signedTransaction.getSignerAddress(), signedTransaction.hash)), + ); + } + + /** + * Announce aggregate transaction + * **NOTE** A lock fund transaction for this aggregate bonded should exists + * @param signedTransaction Signed aggregate bonded transaction. + * @param listener Websocket listener + * @returns {Observable} + */ + public announceAggregateBonded(signedTransaction: SignedTransaction, listener: Listener): Observable { + return this.transactionHttp.announceAggregateBonded(signedTransaction).pipe( + flatMap(() => listener.aggregateBondedAdded(signedTransaction.getSignerAddress(), signedTransaction.hash)), + ); + } + + /** + * Announce aggregate bonded transaction with lock fund + * @param signedHashLockTransaction Signed hash lock transaction. + * @param signedAggregateTransaction Signed aggregate bonded transaction. + * @param listener Websocket listener + * @returns {Observable} + */ + public announceHashLockAggregateBonded(signedHashLockTransaction: SignedTransaction, + signedAggregateTransaction: SignedTransaction, + listener: Listener): Observable { + return this.announce(signedHashLockTransaction, listener).pipe( + flatMap(() => this.announceAggregateBonded(signedAggregateTransaction, listener)), + ); + + } + + /** + * Resolve transaction alias(s) + * @param transaction Transaction to be resolved + * @returns {Observable} + */ + private resolveTransaction(transaction: Transaction): Observable { + if ([TransactionType.AGGREGATE_BONDED, TransactionType.AGGREGATE_COMPLETE].includes(transaction.type)) { + if ((transaction as AggregateTransaction).innerTransactions.find((tx) => this.checkShouldResolve((tx as Transaction)))) { + return this.resolvedFromReceipt(transaction, transaction.transactionInfo!.index); + } + return of(transaction); + } + return this.checkShouldResolve(transaction) ? this.resolvedFromReceipt(transaction, 0) : of(transaction); + } + + /** + * @internal + * Check if receiptHttp needs to be called to resolve transaction alias + * @param transaction Transaction + * @return {boolean} + */ + private checkShouldResolve(transaction: Transaction): boolean { + switch (transaction.type) { + case TransactionType.LINK_ACCOUNT: + case TransactionType.ACCOUNT_METADATA_TRANSACTION: + case TransactionType.ACCOUNT_RESTRICTION_OPERATION: + case TransactionType.ADDRESS_ALIAS: + case TransactionType.MOSAIC_ALIAS: + case TransactionType.MOSAIC_DEFINITION: + case TransactionType.MODIFY_MULTISIG_ACCOUNT: + case TransactionType.NAMESPACE_METADATA_TRANSACTION: + case TransactionType.REGISTER_NAMESPACE: + return false; + case TransactionType.ACCOUNT_RESTRICTION_ADDRESS: + const accountAddressRestriction = transaction as AccountAddressRestrictionTransaction; + return accountAddressRestriction.restrictionAdditions.find((address) => address instanceof NamespaceId) !== undefined || + accountAddressRestriction.restrictionDeletions.find((address) => address instanceof NamespaceId) !== undefined; + case TransactionType.ACCOUNT_RESTRICTION_MOSAIC: + const accountMosaicRestriction = transaction as AccountAddressRestrictionTransaction; + return accountMosaicRestriction.restrictionAdditions.find((mosaicId) => mosaicId instanceof NamespaceId) !== undefined || + accountMosaicRestriction.restrictionDeletions.find((mosaicId) => mosaicId instanceof NamespaceId) !== undefined; + case TransactionType.LOCK: + return (transaction as LockFundsTransaction).mosaic.id instanceof NamespaceId; + case TransactionType.MOSAIC_ADDRESS_RESTRICTION: + const mosaicAddressRestriction = transaction as MosaicAddressRestrictionTransaction; + return mosaicAddressRestriction.targetAddress instanceof NamespaceId || + mosaicAddressRestriction.mosaicId instanceof NamespaceId; + case TransactionType.MOSAIC_GLOBAL_RESTRICTION: + const mosaicGlobalRestriction = transaction as MosaicGlobalRestrictionTransaction; + return mosaicGlobalRestriction.referenceMosaicId instanceof NamespaceId || + mosaicGlobalRestriction.mosaicId instanceof NamespaceId; + case TransactionType.MOSAIC_METADATA_TRANSACTION: + return (transaction as MosaicMetadataTransaction).targetMosaicId instanceof NamespaceId; + case TransactionType.MOSAIC_SUPPLY_CHANGE: + return (transaction as MosaicSupplyChangeTransaction).mosaicId instanceof NamespaceId; + case TransactionType.SECRET_PROOF: + return (transaction as SecretProofTransaction).recipientAddress instanceof NamespaceId; + case TransactionType.SECRET_LOCK: + const secretLock = transaction as SecretLockTransaction; + return secretLock.recipientAddress instanceof NamespaceId || + secretLock.mosaic.id instanceof NamespaceId; + case TransactionType.TRANSFER: + const transfer = transaction as TransferTransaction; + return transfer.recipientAddress instanceof NamespaceId || + transfer.mosaics.find((mosaic) => mosaic.id instanceof NamespaceId) !== undefined; + default: + throw new Error ('Transaction type not not recogonised.'); + } + } + + /** + * @internal + * Resolve transaction alais(s) from block receipt by calling receiptHttp + * @param transaction Transaction to be resolved + * @param aggregateIndex Aggregate transaction index + * @return {Observable} + */ + private resolvedFromReceipt(transaction: Transaction, aggregateIndex: number): Observable { + return this.receiptHttp.getBlockReceipts(transaction.transactionInfo!.height.toString()).pipe( + map((statement) => transaction.resolveAliases(statement, aggregateIndex)), + ); + } +} diff --git a/src/service/interfaces/ITransactionService.ts b/src/service/interfaces/ITransactionService.ts new file mode 100644 index 0000000000..43bd0ae7fa --- /dev/null +++ b/src/service/interfaces/ITransactionService.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Observable} from 'rxjs'; +import { Listener } from '../../infrastructure/Listener'; +import { AggregateTransaction } from '../../model/transaction/AggregateTransaction'; +import { SignedTransaction } from '../../model/transaction/SignedTransaction'; +import { Transaction } from '../../model/transaction/Transaction'; + +/** + * Transaction Service Interface + */ +export interface ITransactionService { + + /** + * @param transationHashes List of transaction hashes. + * @returns {Observable} + */ + resolveAliases(transationHashes: string[]): Observable; + + /** + * @param signedTransaction Signed transaction to be announced. + * @param listener Websocket listener + * @returns {Observable} + */ + announce(signedTransaction: SignedTransaction, listener: Listener): Observable; + + /** + * Announce aggregate transaction + * @param signedTransaction Signed aggregate bonded transaction. + * @param listener Websocket listener + * @returns {Observable} + */ + announceAggregateBonded(signedTransaction: SignedTransaction, listener: Listener): Observable; + + /** + * Announce aggregate bonded transaction with lock fund + * @param signedHashLockTransaction Signed hash lock transaction. + * @param signedAggregateTransaction Signed aggregate bonded transaction. + * @param listener Websocket listener + * @returns {Observable} + */ + announceHashLockAggregateBonded(signedHashLockTransaction: SignedTransaction, + signedAggregateTransaction: SignedTransaction, + listener: Listener): Observable; +} diff --git a/src/service/service.ts b/src/service/service.ts index 03554176a9..fd6074d5c4 100644 --- a/src/service/service.ts +++ b/src/service/service.ts @@ -19,3 +19,4 @@ export * from './MosaicService'; export * from './AggregateTransactionService'; export * from './MetadataTransactionService'; export * from './MosaicRestrictionTransactionService'; +export * from './TransactionService'; diff --git a/test/infrastructure/receipt/CreateReceiptFromDTO.spec.ts b/test/infrastructure/receipt/CreateReceiptFromDTO.spec.ts index 4152778bb0..8c38aa5651 100644 --- a/test/infrastructure/receipt/CreateReceiptFromDTO.spec.ts +++ b/test/infrastructure/receipt/CreateReceiptFromDTO.spec.ts @@ -23,6 +23,7 @@ import { NetworkType } from '../../../src/model/blockchain/NetworkType'; import { MosaicId } from '../../../src/model/mosaic/MosaicId'; import { AddressAlias } from '../../../src/model/namespace/AddressAlias'; import { MosaicAlias } from '../../../src/model/namespace/MosaicAlias'; +import { NamespaceId } from '../../../src/model/namespace/NamespaceId'; import { ReceiptType } from '../../../src/model/receipt/ReceiptType'; import { UInt64 } from '../../../src/model/UInt64'; @@ -122,8 +123,8 @@ describe('Receipt - CreateStatementFromDTO', () => { }); it('should create Statement', () => { const statement = CreateStatementFromDTO(statementDto, netWorkType); - const unresolvedAddress = statement.addressResolutionStatements[0].unresolved as Address; - const unresolvedMosaicId = statement.mosaicResolutionStatements[0].unresolved as MosaicId; + const unresolvedAddress = statement.addressResolutionStatements[0].unresolved as NamespaceId; + const unresolvedMosaicId = statement.mosaicResolutionStatements[0].unresolved as NamespaceId; expect(statement.transactionStatements.length).to.be.equal(1); expect(statement.addressResolutionStatements.length).to.be.equal(2); @@ -136,8 +137,7 @@ describe('Receipt - CreateStatementFromDTO', () => { expect(statement.transactionStatements[0].receipts[0].type).to.be.equal(ReceiptType.Harvest_Fee); deepEqual(statement.addressResolutionStatements[0].height, UInt64.fromNumericString('1488')); - deepEqual(unresolvedAddress.plain(), - Address.createFromEncoded('9103B60AAF2762688300000000000000000000000000000000').plain()); + deepEqual(unresolvedAddress.toHex(), '83686227AF0AB603'); expect(statement.addressResolutionStatements[0].resolutionEntries.length).to.be.equal(1); expect((statement.addressResolutionStatements[0].resolutionEntries[0].resolved as Address).plain()) .to.be.equal(Address.createFromEncoded('917E7E29A01014C2F300000000000000000000000000000000').plain()); diff --git a/test/model/receipt/Receipt.spec.ts b/test/model/receipt/Receipt.spec.ts index d689180a17..2b0bb8ca00 100644 --- a/test/model/receipt/Receipt.spec.ts +++ b/test/model/receipt/Receipt.spec.ts @@ -18,7 +18,7 @@ import { deepEqual } from 'assert'; import { expect } from 'chai'; import { CreateReceiptFromDTO, - CreateStatementFromDTO + CreateStatementFromDTO, } from '../../../src/infrastructure/receipt/CreateReceiptFromDTO'; import { Account } from '../../../src/model/account/Account'; import { Address } from '../../../src/model/account/Address'; @@ -354,7 +354,7 @@ describe('Receipt', () => { const statement = CreateStatementFromDTO(statementDTO, netWorkType); const receipt = statement.addressResolutionStatements[0]; const hash = receipt.generateHash(NetworkType.MAIN_NET); - expect(hash).to.be.equal('6967470641BC527768CDC29998F4A3350813FDF2E40D1C97AB0BBA36B9AF649E'); + expect(hash).to.be.equal('952225717E26295B97F9A35E719CA1319114CCF23C927BCBD14E7A7AA4BAC617'); }); it('should generate hash for TransactionStatement', () => { diff --git a/test/model/receipt/ResolutionStatement.spec.ts b/test/model/receipt/ResolutionStatement.spec.ts new file mode 100644 index 0000000000..65d5d57fe1 --- /dev/null +++ b/test/model/receipt/ResolutionStatement.spec.ts @@ -0,0 +1,219 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { UnresolvedMapping } from '../../../src/core/utils/UnresolvedMapping'; +import { CreateStatementFromDTO } from '../../../src/infrastructure/receipt/CreateReceiptFromDTO'; +import { Account } from '../../../src/model/account/Account'; +import { NetworkType } from '../../../src/model/blockchain/NetworkType'; +import { Address, MosaicId, NamespaceId, ResolutionType } from '../../../src/model/model'; + +describe('ResolutionStatement', () => { + let account: Account; + let transactionStatementsDTO; + let addressResolutionStatementsDTO; + let mosaicResolutionStatementsDTO; + let statementDTO; + + before(() => { + account = Account.createFromPrivateKey('81C18245507F9C15B61BDEDAFA2C10D9DC2C4E401E573A10935D45AA2A461FD5', NetworkType.MIJIN_TEST); + transactionStatementsDTO = [ + { + statement: { + height: '1473', + source: { + primaryId: 0, + secondaryId: 0, + }, + receipts: [ + { + version: 1, + type: 8515, + targetPublicKey: 'B2708D49C46F8AB5CDBD7A09C959EEA12E4A782592F3D1D3D17D54622E655D7F', + mosaicId: '504677C3281108DB', + amount: '0', + }, + ], + }, + }, + ]; + addressResolutionStatementsDTO = [ + { + statement: { + height: '1473', + unresolved: '9156258DE356F030A500000000000000000000000000000000', + resolutionEntries: [ + { + source: { + primaryId: 1, + secondaryId: 0, + }, + resolved: '901D8D4741F80299E66BF7FEEB4F30943DA7B68E068B182319', + }, + ], + }, + }, + ]; + mosaicResolutionStatementsDTO = [ + { + statement: { + height: '1473', + unresolved: '85BBEA6CC462B244', + resolutionEntries: [ + { + source: { + primaryId: 1, + secondaryId: 0, + }, + resolved: '504677C3281108DB', + }, + { + source: { + primaryId: 3, + secondaryId: 5, + }, + resolved: '401F622A3111A3E4', + }, + ], + }, + }, + { + statement: { + height: '1473', + unresolved: 'E81F622A5B11A340', + resolutionEntries: [ + { + source: { + primaryId: 3, + secondaryId: 1, + }, + resolved: '756482FB80FD406C', + }, + ], + }, + }, + { + statement: { + height: '1500', + unresolved: '85BBEA6CC462B244', + resolutionEntries: [ + { + source: { + primaryId: 1, + secondaryId: 1, + }, + resolved: '0DC67FBE1CAD29E5', + }, + { + source: { + primaryId: 1, + secondaryId: 4, + }, + resolved: '7CDF3B117A3C40CC', + }, + { + source: { + primaryId: 1, + secondaryId: 7, + }, + resolved: '0DC67FBE1CAD29E5', + }, + { + source: { + primaryId: 2, + secondaryId: 4, + }, + resolved: '7CDF3B117A3C40CC', + }, + ], + }, + }, + ]; + + statementDTO = { + transactionStatements: transactionStatementsDTO, + addressResolutionStatements: addressResolutionStatementsDTO, + mosaicResolutionStatements: mosaicResolutionStatementsDTO, + }; + }); + + it('should get resolve entry when both primaryId and secondaryId matched', () => { + const statement = CreateStatementFromDTO(statementDTO, NetworkType.MIJIN_TEST); + const entry = statement.addressResolutionStatements[0].getResolutionEntryById(1, 0); + + expect(entry!.resolved instanceof Address).to.be.true; + expect((entry!.resolved as Address).equals(account.address)).to.be.true; + }); + + it('should get resolved entry when primaryId is greater than max', () => { + const statement = CreateStatementFromDTO(statementDTO, NetworkType.MIJIN_TEST); + const entry = statement.mosaicResolutionStatements[0].getResolutionEntryById(4, 0); + expect(entry!.source.primaryId).to.be.equal(3); + expect(entry!.source.secondaryId).to.be.equal(5); + expect(entry!.resolved instanceof MosaicId).to.be.true; + expect((entry!.resolved as MosaicId).equals(new MosaicId('401F622A3111A3E4'))).to.be.true; + }); + + it('should get resolved entry when primaryId is in middle of 2 pirmaryIds', () => { + const statement = CreateStatementFromDTO(statementDTO, NetworkType.MIJIN_TEST); + const entry = statement.mosaicResolutionStatements[0].getResolutionEntryById(2, 1); + expect(entry!.source.primaryId).to.be.equal(1); + expect(entry!.source.secondaryId).to.be.equal(0); + expect(entry!.resolved instanceof MosaicId).to.be.true; + expect((entry!.resolved as MosaicId).equals(new MosaicId('504677C3281108DB'))).to.be.true; + }); + + it('should get resolved entry when primaryId matches but not secondaryId', () => { + const statement = CreateStatementFromDTO(statementDTO, NetworkType.MIJIN_TEST); + const entry = statement.mosaicResolutionStatements[0].getResolutionEntryById(3, 6); + expect(entry!.source.primaryId).to.be.equal(3); + expect(entry!.source.secondaryId).to.be.equal(5); + expect(entry!.resolved instanceof MosaicId).to.be.true; + expect((entry!.resolved as MosaicId).equals(new MosaicId('401F622A3111A3E4'))).to.be.true; + }); + + it('should get resolved entry when primaryId matches but secondaryId less than minimum', () => { + const statement = CreateStatementFromDTO(statementDTO, NetworkType.MIJIN_TEST); + const entry = statement.mosaicResolutionStatements[0].getResolutionEntryById(3, 1); + expect(entry!.source.primaryId).to.be.equal(1); + expect(entry!.source.secondaryId).to.be.equal(0); + expect(entry!.resolved instanceof MosaicId).to.be.true; + expect((entry!.resolved as MosaicId).equals(new MosaicId('504677C3281108DB'))).to.be.true; + }); + + it('should return undefined', () => { + const statement = CreateStatementFromDTO(statementDTO, NetworkType.MIJIN_TEST); + const entry = statement.addressResolutionStatements[0].getResolutionEntryById(0, 0); + expect(entry).to.be.undefined; + }); + + it('resolution change in the block (more than one AGGREGATE)', () => { + const statement = CreateStatementFromDTO(statementDTO, NetworkType.MIJIN_TEST); + const resolution = statement.mosaicResolutionStatements[2]; + expect((resolution.getResolutionEntryById(1, 1)!.resolved as MosaicId).toHex()).to.be.equal('0DC67FBE1CAD29E5'); + expect((resolution.getResolutionEntryById(1, 4)!.resolved as MosaicId).toHex()).to.be.equal('7CDF3B117A3C40CC'); + expect((resolution.getResolutionEntryById(1, 7)!.resolved as MosaicId).toHex()).to.be.equal('0DC67FBE1CAD29E5'); + expect((resolution.getResolutionEntryById(2, 1)!.resolved as MosaicId).toHex()).to.be.equal('0DC67FBE1CAD29E5'); + expect((resolution.getResolutionEntryById(2, 4)!.resolved as MosaicId).toHex()).to.be.equal('7CDF3B117A3C40CC'); + + expect((resolution.getResolutionEntryById(3, 0)!.resolved as MosaicId).toHex()).to.be.equal('7CDF3B117A3C40CC'); + expect((resolution.getResolutionEntryById(2, 2)!.resolved as MosaicId).toHex()).to.be.equal('0DC67FBE1CAD29E5'); + expect(resolution.getResolutionEntryById(1, 0)).to.be.undefined; + expect((resolution.getResolutionEntryById(1, 6)!.resolved as MosaicId).toHex()).to.be.equal('7CDF3B117A3C40CC'); + expect((resolution.getResolutionEntryById(1, 2)!.resolved as MosaicId).toHex()).to.be.equal('0DC67FBE1CAD29E5'); + }); + +}); diff --git a/test/model/receipt/Statement.spec.ts b/test/model/receipt/Statement.spec.ts new file mode 100644 index 0000000000..b0ef2c0e2e --- /dev/null +++ b/test/model/receipt/Statement.spec.ts @@ -0,0 +1,203 @@ +/* + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { UnresolvedMapping } from '../../../src/core/utils/UnresolvedMapping'; +import { CreateStatementFromDTO } from '../../../src/infrastructure/receipt/CreateReceiptFromDTO'; +import { Account } from '../../../src/model/account/Account'; +import { NetworkType } from '../../../src/model/blockchain/NetworkType'; +import { Address, MosaicId, NamespaceId, ResolutionType } from '../../../src/model/model'; + +describe('Statement', () => { + let account: Account; + let transactionStatementsDTO; + let addressResolutionStatementsDTO; + let mosaicResolutionStatementsDTO; + let statementDTO; + + before(() => { + account = Account.createFromPrivateKey('81C18245507F9C15B61BDEDAFA2C10D9DC2C4E401E573A10935D45AA2A461FD5', NetworkType.MIJIN_TEST); + transactionStatementsDTO = [ + { + statement: { + height: '1473', + source: { + primaryId: 0, + secondaryId: 0, + }, + receipts: [ + { + version: 1, + type: 8515, + targetPublicKey: 'B2708D49C46F8AB5CDBD7A09C959EEA12E4A782592F3D1D3D17D54622E655D7F', + mosaicId: '504677C3281108DB', + amount: '0', + }, + ], + }, + }, + ]; + addressResolutionStatementsDTO = [ + { + statement: { + height: '1473', + unresolved: '9156258DE356F030A500000000000000000000000000000000', + resolutionEntries: [ + { + source: { + primaryId: 1, + secondaryId: 0, + }, + resolved: '901D8D4741F80299E66BF7FEEB4F30943DA7B68E068B182319', + }, + ], + }, + }, + ]; + mosaicResolutionStatementsDTO = [ + { + statement: { + height: '1473', + unresolved: '85BBEA6CC462B244', + resolutionEntries: [ + { + source: { + primaryId: 1, + secondaryId: 0, + }, + resolved: '504677C3281108DB', + }, + ], + }, + }, + { + statement: { + height: '1473', + unresolved: 'E81F622A5B11A340', + resolutionEntries: [ + { + source: { + primaryId: 1, + secondaryId: 0, + }, + resolved: '756482FB80FD406C', + }, + ], + }, + }, + ]; + + statementDTO = { + transactionStatements: transactionStatementsDTO, + addressResolutionStatements: addressResolutionStatementsDTO, + mosaicResolutionStatements: mosaicResolutionStatementsDTO, + }; + }); + + it('should get resolved address from receipt', () => { + const unresolvedAddress = UnresolvedMapping.toUnresolvedAddress('9156258DE356F030A500000000000000000000000000000000'); + const statement = CreateStatementFromDTO(statementDTO, NetworkType.MIJIN_TEST); + const resolved = statement.resolveAddress(unresolvedAddress as NamespaceId, '1473', 0); + + expect(resolved instanceof Address).to.be.true; + expect((resolved as Address).equals(account.address)).to.be.true; + }); + + it('should get resolved address from receipt without Harvesting_Fee', () => { + const statementWithoutHarvesting = { + transactionStatements: [], + addressResolutionStatements: [ + { + statement: { + height: '1473', + unresolved: '9156258DE356F030A500000000000000000000000000000000', + resolutionEntries: [ + { + source: { + primaryId: 1, + secondaryId: 0, + }, + resolved: '901D8D4741F80299E66BF7FEEB4F30943DA7B68E068B182319', + }, + ], + }, + }, + ], + mosaicResolutionStatements: [], + }; + const unresolvedAddress = UnresolvedMapping.toUnresolvedAddress('9156258DE356F030A500000000000000000000000000000000'); + const statement = CreateStatementFromDTO(statementWithoutHarvesting, NetworkType.MIJIN_TEST); + const resolved = statement.resolveAddress(unresolvedAddress as NamespaceId, '1473', 0); + + expect(resolved instanceof Address).to.be.true; + expect((resolved as Address).equals(account.address)).to.be.true; + }); + + it('should get resolved mosaic from receipt', () => { + const unresolvedMosaic = UnresolvedMapping.toUnresolvedMosaic('E81F622A5B11A340'); + const statement = CreateStatementFromDTO(statementDTO, NetworkType.MIJIN_TEST); + const resolved = statement.resolveMosaicId(unresolvedMosaic as NamespaceId, '1473', 0); + + expect(resolved instanceof MosaicId).to.be.true; + expect((resolved as MosaicId).equals(new MosaicId('756482FB80FD406C'))).to.be.true; + }); + + it('should get resolved mosaic from receipt without Harvesting_Fee', () => { + const statementWithoutHarvesting = { + transactionStatements: [], + addressResolutionStatements: [], + mosaicResolutionStatements: [ + { + statement: { + height: '1473', + unresolved: '85BBEA6CC462B244', + resolutionEntries: [ + { + source: { + primaryId: 1, + secondaryId: 0, + }, + resolved: '504677C3281108DB', + }, + ], + }, + }, + { + statement: { + height: '1473', + unresolved: 'E81F622A5B11A340', + resolutionEntries: [ + { + source: { + primaryId: 1, + secondaryId: 0, + }, + resolved: '756482FB80FD406C', + }, + ], + }, + }, + ], + }; + const unresolvedMosaic = UnresolvedMapping.toUnresolvedMosaic('E81F622A5B11A340'); + const statement = CreateStatementFromDTO(statementWithoutHarvesting, NetworkType.MIJIN_TEST); + const resolved = statement.resolveMosaicId(unresolvedMosaic as NamespaceId, '1473', 0); + + expect(resolved instanceof MosaicId).to.be.true; + expect((resolved as MosaicId).equals(new MosaicId('756482FB80FD406C'))).to.be.true; + }); + +}); diff --git a/test/model/transaction/Transaction.spec.ts b/test/model/transaction/Transaction.spec.ts index 5815da8f8b..1858bdef7d 100644 --- a/test/model/transaction/Transaction.spec.ts +++ b/test/model/transaction/Transaction.spec.ts @@ -15,6 +15,8 @@ */ import { expect } from 'chai'; +import { Observable } from 'rxjs/internal/Observable'; +import { Convert } from '../../../src/core/format/Convert'; import { Account } from '../../../src/model/account/Account'; import { Address } from '../../../src/model/account/Address'; import { NetworkType } from '../../../src/model/blockchain/NetworkType'; @@ -28,7 +30,6 @@ import { TransactionType } from '../../../src/model/transaction/TransactionType' import { TransferTransaction } from '../../../src/model/transaction/TransferTransaction'; import { UInt64 } from '../../../src/model/UInt64'; import { TestingAccount } from '../../conf/conf.spec'; -import { Convert } from '../../../src/core/format/Convert'; describe('Transaction', () => { let account: Account; @@ -254,7 +255,6 @@ describe('Transaction', () => { // expected values const knownHash_sha3 = '709373248659274C5933BEA2920942D6C7B48B9C2DA4BAEE233510E71495931F'; - const knownHash_keccak = '787423372BEC0CB2BE3EEA58E773074E121989AF29E5E5BD9EE660C1E3A0AF93'; const generationHashBytes = Array.from(Convert.hexToUint8('988C4CDCE4D188013C13DE7914C7FD4D626169EF256722F61C52EFBE06BD5A2C')); const generationHashBytes_mt = Array.from(Convert.hexToUint8('17FA4747F5014B50413CCF968749604D728D7065DC504291EEE556899A534CBB')); @@ -403,4 +403,7 @@ class FakeTransaction extends Transaction { protected generateEmbeddedBytes(): Uint8Array { throw new Error('Not implemented'); } + resolveAliases(): TransferTransaction { + throw new Error('Not implemented'); + } }