Skip to content

Commit 847006a

Browse files
authored
Merge pull request #363 from NEMStudios/task/g357_standardize_transaction_service
Task/g357 standardize transaction service
2 parents df4e16e + 5c8c183 commit 847006a

37 files changed

+1823
-37
lines changed

e2e/service/TransactionService.spec.ts

Lines changed: 561 additions & 0 deletions
Large diffs are not rendered by default.

src/core/utils/TransactionMapping.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { CreateTransactionFromDTO } from '../../infrastructure/transaction/Creat
1818
import { CreateTransactionFromPayload } from '../../infrastructure/transaction/CreateTransactionFromPayload';
1919
import { InnerTransaction } from '../../model/transaction/InnerTransaction';
2020
import { Transaction } from '../../model/transaction/Transaction';
21-
import { SignSchema } from '../crypto/SignSchema';
2221

2322
export class TransactionMapping {
2423

src/core/utils/UnresolvedMapping.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
* limitations under the License.
1515
*/
1616
import { Address } from '../../model/account/Address';
17+
import { NetworkType } from '../../model/blockchain/NetworkType';
1718
import { MosaicId } from '../../model/mosaic/MosaicId';
1819
import { NamespaceId } from '../../model/namespace/NamespaceId';
1920
import { Convert } from '../format/Convert';
2021
import { RawAddress } from '../format/RawAddress';
21-
import { NetworkType } from "../../model/blockchain/NetworkType";
2222

2323
/**
2424
* @internal

src/infrastructure/Listener.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,15 +239,18 @@ export class Listener {
239239
* it emits a new Transaction in the event stream.
240240
*
241241
* @param address address we listen when a transaction is in confirmed state
242+
* @param transactionHash transaction hash for the filter
242243
* @return an observable stream of Transaction with state confirmed
243244
*/
244-
public confirmed(address: Address): Observable<Transaction> {
245+
public confirmed(address: Address, transactionHash?: string): Observable<Transaction> {
245246
this.subscribeTo(`confirmedAdded/${address.plain()}`);
246247
return this.messageSubject.asObservable().pipe(
247248
filter((_) => _.channelName === ListenerChannelName.confirmedAdded),
248249
filter((_) => _.message instanceof Transaction),
249250
map((_) => _.message as Transaction),
250-
filter((_) => this.transactionFromAddress(_, address)));
251+
filter((_) => this.transactionFromAddress(_, address)),
252+
filter((_) => _.transactionInfo!.hash === transactionHash || transactionHash === undefined),
253+
);
251254
}
252255

253256
/**
@@ -289,15 +292,18 @@ export class Listener {
289292
* it emits a new {@link AggregateTransaction} in the event stream.
290293
*
291294
* @param address address we listen when a transaction with missing signatures state
295+
* @param transactionHash transaction hash for the filter
292296
* @return an observable stream of AggregateTransaction with missing signatures state
293297
*/
294-
public aggregateBondedAdded(address: Address): Observable<AggregateTransaction> {
298+
public aggregateBondedAdded(address: Address, transactionHash?: string): Observable<AggregateTransaction> {
295299
this.subscribeTo(`partialAdded/${address.plain()}`);
296300
return this.messageSubject.asObservable().pipe(
297301
filter((_) => _.channelName === ListenerChannelName.aggregateBondedAdded),
298302
filter((_) => _.message instanceof AggregateTransaction),
299303
map((_) => _.message as AggregateTransaction),
300-
filter((_) => this.transactionFromAddress(_, address)));
304+
filter((_) => this.transactionFromAddress(_, address)),
305+
filter((_) => _.transactionInfo!.hash === transactionHash || transactionHash === undefined),
306+
);
301307
}
302308

303309
/**

src/infrastructure/receipt/CreateReceiptFromDTO.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,10 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { UnresolvedMapping } from '../../core/utils/UnresolvedMapping';
1718
import { Address } from '../../model/account/Address';
1819
import {PublicAccount} from '../../model/account/PublicAccount';
1920
import {MosaicId} from '../../model/mosaic/MosaicId';
20-
import { AddressAlias } from '../../model/namespace/AddressAlias';
21-
import { AliasType } from '../../model/namespace/AliasType';
22-
import { MosaicAlias } from '../../model/namespace/MosaicAlias';
2321
import { NamespaceId } from '../../model/namespace/NamespaceId';
2422
import { ArtifactExpiryReceipt } from '../../model/receipt/ArtifactExpiryReceipt';
2523
import { BalanceChangeReceipt } from '../../model/receipt/BalanceChangeReceipt';
@@ -95,7 +93,7 @@ const createResolutionStatement = (statementDTO, resolutionType): ResolutionStat
9593
return new ResolutionStatement(
9694
ResolutionType.Address,
9795
UInt64.fromNumericString(statementDTO.height),
98-
Address.createFromEncoded(statementDTO.unresolved),
96+
extractUnresolvedAddress(statementDTO.unresolved),
9997
statementDTO.resolutionEntries.map((entry) => {
10098
return new ResolutionEntry(Address.createFromEncoded(entry.resolved),
10199
new ReceiptSource(entry.source.primaryId, entry.source.secondaryId));
@@ -105,7 +103,7 @@ const createResolutionStatement = (statementDTO, resolutionType): ResolutionStat
105103
return new ResolutionStatement(
106104
ResolutionType.Mosaic,
107105
UInt64.fromNumericString(statementDTO.height),
108-
new MosaicId(statementDTO.unresolved),
106+
UnresolvedMapping.toUnresolvedMosaic(statementDTO.unresolved),
109107
statementDTO.resolutionEntries.map((entry) => {
110108
return new ResolutionEntry(new MosaicId(entry.resolved),
111109
new ReceiptSource(entry.source.primaryId, entry.source.secondaryId));
@@ -197,6 +195,12 @@ const createInflationReceipt = (receiptDTO): Receipt => {
197195
);
198196
};
199197

198+
/**
199+
* @internal
200+
* @param receiptType receipt type
201+
* @param id Artifact id
202+
* @returns {MosaicId | NamespaceId}
203+
*/
200204
const extractArtifactId = (receiptType: ReceiptType, id: string): MosaicId | NamespaceId => {
201205
switch (receiptType) {
202206
case ReceiptType.Mosaic_Expired:
@@ -208,3 +212,21 @@ const extractArtifactId = (receiptType: ReceiptType, id: string): MosaicId | Nam
208212
throw new Error('Receipt type is not supported.');
209213
}
210214
};
215+
216+
/**
217+
* @interal
218+
* @param unresolvedAddress unresolved address
219+
* @returns {Address | NamespaceId}
220+
*/
221+
const extractUnresolvedAddress = (unresolvedAddress: any): Address | NamespaceId => {
222+
if (typeof unresolvedAddress === 'string') {
223+
return UnresolvedMapping.toUnresolvedAddress(unresolvedAddress);
224+
} else if (typeof unresolvedAddress === 'object') { // Is JSON object
225+
if (unresolvedAddress.hasOwnProperty('address')) {
226+
return Address.createFromRawAddress(unresolvedAddress.address);
227+
} else if (unresolvedAddress.hasOwnProperty('id')) {
228+
return NamespaceId.createFromEncoded(unresolvedAddress.id);
229+
}
230+
}
231+
throw new Error(`UnresolvedAddress: ${unresolvedAddress} type is not recognised`);
232+
};

src/infrastructure/transaction/CreateTransactionFromDTO.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ const CreateStandaloneTransactionFromDTO = (transactionDTO, transactionInfo): Tr
175175
transactionDTO.version,
176176
Deadline.createFromDTO(transactionDTO.deadline),
177177
UInt64.fromNumericString(transactionDTO.maxFee || '0'),
178-
new MosaicId(transactionDTO.mosaicId),
178+
UnresolvedMapping.toUnresolvedMosaic(transactionDTO.mosaicId),
179179
transactionDTO.action,
180180
UInt64.fromNumericString(transactionDTO.delta),
181181
transactionDTO.signature,

src/model/receipt/ResolutionStatement.ts

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,17 @@
1515
*/
1616

1717
import { sha3_256 } from 'js-sha3';
18+
import { UnresolvedMapping } from '../../core/utils/UnresolvedMapping';
1819
import { GeneratorUtils } from '../../infrastructure/catbuffer/GeneratorUtils';
1920
import { Address } from '../account/Address';
21+
import { NetworkType } from '../blockchain/NetworkType';
2022
import { MosaicId } from '../mosaic/MosaicId';
2123
import { NamespaceId } from '../namespace/NamespaceId';
2224
import { UInt64 } from '../UInt64';
2325
import { ReceiptType } from './ReceiptType';
2426
import { ReceiptVersion } from './ReceiptVersion';
2527
import { ResolutionEntry } from './ResolutionEntry';
2628
import { ResolutionType } from './ResolutionType';
27-
import { NetworkType } from "../blockchain/NetworkType";
28-
import { UnresolvedMapping } from "../../core/utils/UnresolvedMapping";
2929

3030
/**
3131
* 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 {
8484
return hasher.hex().toUpperCase();
8585
}
8686

87+
/**
88+
* @internal
89+
* Find resolution entry for given primaryId and secondaryId
90+
* @param primaryId Primary id
91+
* @param secondaryId Secondary id
92+
* @returns {ResolutionEntry | undefined}
93+
*/
94+
public getResolutionEntryById(primaryId: number, secondaryId: number): ResolutionEntry | undefined {
95+
/*
96+
Primary id and secondary id do not specifically map to the exact transaction index on the same block.
97+
The ids are just the order of the resolution reflecting on the order of transactions (ordered by index).
98+
E.g 1 - Bob -> 1 random.token -> Alice
99+
2 - Carol -> 1 random.token > Denis
100+
Based on above example, 2 transactions (index 0 & 1) are created on the same block, however, only 1
101+
resolution entry get generated for both.
102+
*/
103+
const resolvedPrimaryId = this.getMaxAvailablePrimaryId(primaryId);
104+
105+
/*
106+
If no primaryId found, it means there's no resolution entry available for the process. Invalid entry.
107+
108+
e.g. Given:
109+
Entries: [{P:2, S:0}, {P:5, S:6}]
110+
Transaction: [Inx:1(0+1), AggInx:0]
111+
It should return Entry: undefined
112+
*/
113+
if (resolvedPrimaryId === 0) {
114+
return undefined;
115+
} else if (primaryId > resolvedPrimaryId) {
116+
/*
117+
If the transaction index is greater than the overall most recent source primary id.
118+
Use the most recent resolution entry (Max.PrimaryId + Max.SecondaryId)
119+
120+
e.g. Given:
121+
Entries: [{P:1, S:0}, {P:2, S:0}, {P:4, S:2}, {P:4, S:4} {P:7, S:6}]
122+
Transaction: [Inx:5(4+1), AggInx:0]
123+
It should return Entry: {P:4, S:4}
124+
125+
e.g. Given:
126+
Entries: [{P:1, S:0}, {P:2, S:0}, {P:4, S:2}, {P:4, S:4}, {P:7, S:6}]
127+
Transaction: [Inx:3(2+1), AggInx:0]
128+
It should return Entry: {P:2, S:0}
129+
*/
130+
return this.resolutionEntries
131+
.find((entry) => entry.source.primaryId === resolvedPrimaryId &&
132+
entry.source.secondaryId === this.getMaxSecondaryIdByPrimaryId(resolvedPrimaryId));
133+
}
134+
135+
// When transaction index matches a primaryId, get the most recent secondaryId (resolvedPrimaryId can only <= primaryId)
136+
const resolvedSecondaryId = this.getMaxSecondaryIdByPrimaryIdAndSecondaryId(resolvedPrimaryId, secondaryId);
137+
138+
/*
139+
If no most recent secondaryId matched transaction index, find previous resolution entry (most recent).
140+
This means the resolution entry for the specific inner transaction (inside Aggregate) /
141+
was generated previously outside the aggregate. It should return the previous entry (previous primaryId)
142+
143+
e.g. Given:
144+
Entries: [{P:1, S:0}, {P:2, S:0}, {P:5, S:6}]
145+
Transaction: [Inx:5(4+1), AggInx:3(2+1)]
146+
It should return Entry: {P:2, S:0}
147+
*/
148+
if (resolvedSecondaryId === 0 && resolvedSecondaryId !== secondaryId) {
149+
const lastPrimaryId = this.getMaxAvailablePrimaryId(resolvedPrimaryId - 1);
150+
return this.resolutionEntries.find((entry) => entry.source.primaryId === lastPrimaryId &&
151+
entry.source.secondaryId === this.getMaxSecondaryIdByPrimaryId(lastPrimaryId));
152+
}
153+
154+
/*
155+
Found a matched resolution entry on both primaryId and secondaryId
156+
157+
e.g. Given:
158+
Entries: [{P:1, S:0}, {P:2, S:0}, {P:5, S:6}]
159+
Transaction: [Inx:5(4+1), AggInx:6(2+1)]
160+
It should return Entry: {P:5, S:6}
161+
*/
162+
return this.resolutionEntries
163+
.find((entry) => entry.source.primaryId === resolvedPrimaryId && entry.source.secondaryId === resolvedSecondaryId);
164+
}
165+
166+
/**
167+
* @internal
168+
* Get max secondary id by a given primaryId
169+
* @param primaryId Primary source id
170+
* @returns {number}
171+
*/
172+
private getMaxSecondaryIdByPrimaryId(primaryId: number): number {
173+
return Math.max(...this.resolutionEntries.filter((entry) => entry.source.primaryId === primaryId)
174+
.map((filtered) => filtered.source.secondaryId));
175+
}
176+
177+
/**
178+
* Get most `recent` available secondary id by a given primaryId
179+
* @param primaryId Primary source id
180+
* @param secondaryId Secondary source id
181+
* @returns {number}
182+
*/
183+
private getMaxSecondaryIdByPrimaryIdAndSecondaryId(primaryId: number, secondaryId: number): number {
184+
return Math.max(...this.resolutionEntries.filter((entry) => entry.source.primaryId === primaryId)
185+
.map((filtered) => secondaryId >= filtered.source.secondaryId ? filtered.source.secondaryId : 0));
186+
}
187+
188+
/**
189+
* @internal
190+
* Get most `recent` primary source id by a given id (transaction index) as PrimaryId might not be the same as block transaction index.
191+
* @param primaryId Primary source id
192+
* @returns {number}
193+
*/
194+
private getMaxAvailablePrimaryId(primaryId: number): number {
195+
return Math.max(...this.resolutionEntries
196+
.map((entry) => primaryId >= entry.source.primaryId ? entry.source.primaryId : 0));
197+
}
198+
87199
/**
88200
* @internal
89201
* Generate buffer for unresulved

src/model/receipt/Statement.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { Address } from '../account/Address';
18+
import { Mosaic } from '../mosaic/Mosaic';
19+
import { MosaicId } from '../mosaic/MosaicId';
20+
import { NamespaceId } from '../namespace/NamespaceId';
1721
import { ResolutionStatement } from './ResolutionStatement';
22+
import { ResolutionType } from './ResolutionType';
1823
import { TransactionStatement } from './TransactionStatement';
1924

2025
export class Statement {
@@ -39,4 +44,99 @@ export class Statement {
3944
*/
4045
public readonly mosaicResolutionStatements: ResolutionStatement[]) {
4146
}
47+
48+
/**
49+
* Resolve unresolvedAddress from statement
50+
* @param unresolvedAddress Unresolved address
51+
* @param height Block height
52+
* @param transactionIndex Transaction index
53+
* @param aggregateTransactionIndex Aggregate transaction index
54+
* @returns {Address}
55+
*/
56+
public resolveAddress(unresolvedAddress: Address | NamespaceId,
57+
height: string,
58+
transactionIndex: number,
59+
aggregateTransactionIndex: number = 0): Address {
60+
return unresolvedAddress instanceof NamespaceId ?
61+
this.getResolvedFromReceipt(ResolutionType.Address, unresolvedAddress as NamespaceId,
62+
transactionIndex, height, aggregateTransactionIndex) as Address :
63+
unresolvedAddress;
64+
}
65+
66+
/**
67+
* Resolve unresolvedMosaicId from statement
68+
* @param unresolvedMosaicId Unresolved mosaic id
69+
* @param height Block height
70+
* @param transactionIndex Transaction index
71+
* @param aggregateTransactionIndex Aggregate transaction index
72+
* @returns {MosaicId}
73+
*/
74+
public resolveMosaicId(unresolvedMosaicId: MosaicId | NamespaceId,
75+
height: string,
76+
transactionIndex: number,
77+
aggregateTransactionIndex: number = 0): MosaicId {
78+
return unresolvedMosaicId instanceof NamespaceId ?
79+
this.getResolvedFromReceipt(ResolutionType.Mosaic, unresolvedMosaicId as NamespaceId,
80+
transactionIndex, height, aggregateTransactionIndex) as MosaicId :
81+
unresolvedMosaicId;
82+
}
83+
84+
/**
85+
* Resolve unresolvedMosaic from statement
86+
* @param unresolvedMosaic Unresolved mosaic
87+
* @param height Block height
88+
* @param transactionIndex Transaction index
89+
* @param aggregateTransactionIndex Aggregate transaction index
90+
* @returns {Mosaic}
91+
*/
92+
public resolveMosaic(unresolvedMosaic: Mosaic,
93+
height: string,
94+
transactionIndex: number,
95+
aggregateTransactionIndex: number = 0): Mosaic {
96+
return unresolvedMosaic.id instanceof NamespaceId ?
97+
new Mosaic(this.getResolvedFromReceipt(ResolutionType.Mosaic, unresolvedMosaic.id as NamespaceId,
98+
transactionIndex, height, aggregateTransactionIndex) as MosaicId, unresolvedMosaic.amount) :
99+
unresolvedMosaic;
100+
}
101+
102+
/**
103+
* @internal
104+
* Extract resolved address | mosaic from block receipt
105+
* @param resolutionType Resolution type: Address / Mosaic
106+
* @param unresolved Unresolved address / mosaicId
107+
* @param transactionIndex Transaction index
108+
* @param height Transaction height
109+
* @param aggregateTransactionIndex Transaction index for aggregate
110+
* @returns {MosaicId | Address}
111+
*/
112+
private getResolvedFromReceipt(resolutionType: ResolutionType,
113+
unresolved: NamespaceId,
114+
transactionIndex: number,
115+
height: string,
116+
aggregateTransactionIndex?: number): MosaicId | Address {
117+
118+
const resolutionStatement = (resolutionType === ResolutionType.Address ? this.addressResolutionStatements :
119+
this.mosaicResolutionStatements).find((resolution) => resolution.height.toString() === height &&
120+
(resolution.unresolved as NamespaceId).equals(unresolved));
121+
122+
if (!resolutionStatement) {
123+
throw new Error(`No resolution statement found on block: ${height} for unresolved: ${unresolved.toHex()}`);
124+
}
125+
126+
// If only one entry exists on the statement, just return
127+
if (resolutionStatement.resolutionEntries.length === 1) {
128+
return resolutionStatement.resolutionEntries[0].resolved;
129+
}
130+
131+
// Get the most recent resolution entry
132+
const resolutionEntry = resolutionStatement.getResolutionEntryById(
133+
aggregateTransactionIndex !== undefined ? aggregateTransactionIndex + 1 : transactionIndex + 1,
134+
aggregateTransactionIndex !== undefined ? transactionIndex + 1 : 0,
135+
);
136+
137+
if (!resolutionEntry) {
138+
throw new Error(`No resolution entry found on block: ${height} for unresolved: ${unresolved.toHex()}`);
139+
}
140+
return resolutionEntry.resolved;
141+
}
42142
}

0 commit comments

Comments
 (0)