From 28dccca70e5005ac75daab0e52ded57c43b60955 Mon Sep 17 00:00:00 2001 From: Matej Lubej Date: Wed, 5 Jun 2024 11:55:17 +0200 Subject: [PATCH 01/12] Add getTransaction to explorer API --- src/vendors/monitor.ts | 19 +- src/vendors/oasisscan.ts | 31 ++- .../oasisscan/.openapi-generator/FILES | 7 +- src/vendors/oasisscan/apis/AccountsApi.ts | 18 +- .../oasisscan/apis/OperationsEntityApi.ts | 62 ++++++ .../oasisscan/apis/OperationsListApi.ts | 12 +- src/vendors/oasisscan/apis/RuntimeApi.ts | 12 +- src/vendors/oasisscan/apis/index.ts | 1 + .../oasisscan/models/InlineResponse2002.ts | 16 +- .../oasisscan/models/InlineResponse2003.ts | 16 +- .../models/InlineResponse2003Data.ts | 62 +++--- .../oasisscan/models/InlineResponse2004.ts | 16 +- .../models/InlineResponse2005Data.ts | 16 +- .../oasisscan/models/InlineResponse2006.ts | 72 +++++++ .../models/InlineResponse2006Data.ts | 96 ++++++++++ .../oasisscan/models/OperationsEntity.ts | 181 ++++++++++++++++++ src/vendors/oasisscan/models/index.ts | 6 +- src/vendors/oasisscan/swagger.yml | 93 +++++++++ 18 files changed, 635 insertions(+), 101 deletions(-) create mode 100644 src/vendors/oasisscan/apis/OperationsEntityApi.ts create mode 100644 src/vendors/oasisscan/models/InlineResponse2006.ts create mode 100644 src/vendors/oasisscan/models/InlineResponse2006Data.ts create mode 100644 src/vendors/oasisscan/models/OperationsEntity.ts diff --git a/src/vendors/monitor.ts b/src/vendors/monitor.ts index 0443c22016..612a3787e0 100644 --- a/src/vendors/monitor.ts +++ b/src/vendors/monitor.ts @@ -38,11 +38,16 @@ export function getMonitorAPIs(url: string | 'https://monitor.oasis.dev') { return parseValidatorsList(validators) } - async function getTransactionsList(params: { accountId: string; limit: number }): Promise { + async function getTransaction({ hash }: { hash: string }) { + throw new Error('Not implemented') + } + + async function getTransactionsList(params: { accountId: string; limit: number }) { const transactions = await operations.getTransactionsList({ accountId: params.accountId, limit: params.limit, }) + return parseTransactionsList(transactions) } @@ -64,7 +69,15 @@ export function getMonitorAPIs(url: string | 'https://monitor.oasis.dev') { } } - return { accounts, blocks, getAccount, getAllValidators, getTransactionsList, getDelegations } + return { + accounts, + blocks, + getAccount, + getAllValidators, + getTransaction, + getTransactionsList, + getDelegations, + } } export function parseAccount(account: AccountsRow): Account { @@ -79,6 +92,7 @@ export function parseAccount(account: AccountsRow): Account { BigInt(account.delegations_balance) + BigInt(account.debonding_delegations_balance) ).toString(), + nonce: BigInt(account.nonce ?? 0).toString(), } } @@ -163,6 +177,7 @@ export function parseTransactionsList(transactionsList: OperationsRow[]): Transa runtimeName: undefined, runtimeId: undefined, round: undefined, + nonce: undefined, } return parsed }) diff --git a/src/vendors/oasisscan.ts b/src/vendors/oasisscan.ts index 000cc3411e..baf6fef4c1 100644 --- a/src/vendors/oasisscan.ts +++ b/src/vendors/oasisscan.ts @@ -16,6 +16,8 @@ import { OperationsRowMethodEnum, ParaTimeCtxRowMethodEnum, RuntimeTransactionInfoRow, + OperationsEntityApi, + OperationsEntity, } from 'vendors/oasisscan/index' import { throwAPIErrors } from './helpers' @@ -28,6 +30,7 @@ export function getOasisscanAPIs(url: string | 'https://api.oasisscan.com/mainne const accounts = new AccountsApi(explorerConfig) const operations = new OperationsListApi(explorerConfig) + const operationsEntity = new OperationsEntityApi(explorerConfig) const runtime = new RuntimeApi(explorerConfig) async function getAccount(address: string): Promise { @@ -50,7 +53,16 @@ export function getOasisscanAPIs(url: string | 'https://api.oasisscan.com/mainne return data } - async function getTransactionsList(params: { accountId: string; limit: number }): Promise { + async function getTransaction({ hash }: { hash: string }) { + const transaction = await operationsEntity.getTransaction({ + hash, + }) + + const [parsedTx] = parseTransactionsList([transaction.data]) + return parsedTx + } + + async function getTransactionsList(params: { accountId: string; limit: number }) { const transactionsList = await operations.getTransactionsList({ address: params.accountId, size: params.limit, @@ -80,7 +92,15 @@ export function getOasisscanAPIs(url: string | 'https://api.oasisscan.com/mainne } } - return { accounts, operations, getAccount, getAllValidators, getTransactionsList, getDelegations } + return { + accounts, + operations, + getAccount, + getAllValidators, + getTransaction, + getTransactionsList, + getDelegations, + } } export function parseAccount(account: AccountsRow): Account { @@ -94,6 +114,7 @@ export function parseAccount(account: AccountsRow): Account { delegations: parseRoseStringToBaseUnitString(account.escrow), debonding: parseRoseStringToBaseUnitString(account.debonding), total: parseRoseStringToBaseUnitString(account.total), + nonce: BigInt(account.nonce ?? 0).toString(), } } @@ -150,7 +171,9 @@ export const transactionMethodMap: { [ParaTimeCtxRowMethodEnum.ConsensusAccount]: TransactionType.ConsensusAccount, } -export function parseTransactionsList(list: (OperationsRow | RuntimeTransactionInfoRow)[]): Transaction[] { +export function parseTransactionsList( + list: (OperationsRow | RuntimeTransactionInfoRow | OperationsEntity)[], +): Transaction[] { return list.map(t => { if ('ctx' in t) { const parsed: Transaction = { @@ -166,6 +189,7 @@ export function parseTransactionsList(list: (OperationsRow | RuntimeTransactionI runtimeName: t.runtimeName, runtimeId: t.runtimeId, round: t.round, + nonce: undefined, } return parsed } else { @@ -182,6 +206,7 @@ export function parseTransactionsList(list: (OperationsRow | RuntimeTransactionI runtimeName: undefined, runtimeId: undefined, round: undefined, + nonce: BigInt((t as OperationsEntity).nonce ?? 0).toString() } return parsed } diff --git a/src/vendors/oasisscan/.openapi-generator/FILES b/src/vendors/oasisscan/.openapi-generator/FILES index 7c34faa8fe..fd45adf79f 100644 --- a/src/vendors/oasisscan/.openapi-generator/FILES +++ b/src/vendors/oasisscan/.openapi-generator/FILES @@ -1,4 +1,5 @@ apis/AccountsApi.ts +apis/OperationsEntityApi.ts apis/OperationsListApi.ts apis/RuntimeApi.ts apis/index.ts @@ -11,12 +12,14 @@ models/InlineResponse200.ts models/InlineResponse2001.ts models/InlineResponse2001Data.ts models/InlineResponse2002.ts -models/InlineResponse2002Data.ts models/InlineResponse2003.ts +models/InlineResponse2003Data.ts models/InlineResponse2004.ts -models/InlineResponse2004Data.ts models/InlineResponse2005.ts models/InlineResponse2005Data.ts +models/InlineResponse2006.ts +models/InlineResponse2006Data.ts +models/OperationsEntity.ts models/OperationsRow.ts models/ParaTimeCtxRow.ts models/RuntimeTransactionInfoRow.ts diff --git a/src/vendors/oasisscan/apis/AccountsApi.ts b/src/vendors/oasisscan/apis/AccountsApi.ts index 0a42162c93..b3e0dca525 100644 --- a/src/vendors/oasisscan/apis/AccountsApi.ts +++ b/src/vendors/oasisscan/apis/AccountsApi.ts @@ -21,12 +21,12 @@ import { InlineResponse2001, InlineResponse2001FromJSON, InlineResponse2001ToJSON, - InlineResponse2004, - InlineResponse2004FromJSON, - InlineResponse2004ToJSON, InlineResponse2005, InlineResponse2005FromJSON, InlineResponse2005ToJSON, + InlineResponse2006, + InlineResponse2006FromJSON, + InlineResponse2006ToJSON, } from '../models'; export interface GetAccountRequest { @@ -87,7 +87,7 @@ export class AccountsApi extends runtime.BaseAPI { /** */ - async getDebondingDelegationsRaw(requestParameters: GetDebondingDelegationsRequest): Promise> { + async getDebondingDelegationsRaw(requestParameters: GetDebondingDelegationsRequest): Promise> { const queryParameters: any = {}; if (requestParameters.size !== undefined) { @@ -115,19 +115,19 @@ export class AccountsApi extends runtime.BaseAPI { query: queryParameters, }); - return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2005FromJSON(jsonValue)); + return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2006FromJSON(jsonValue)); } /** */ - async getDebondingDelegations(requestParameters: GetDebondingDelegationsRequest): Promise { + async getDebondingDelegations(requestParameters: GetDebondingDelegationsRequest): Promise { const response = await this.getDebondingDelegationsRaw(requestParameters); return await response.value(); } /** */ - async getDelegationsRaw(requestParameters: GetDelegationsRequest): Promise> { + async getDelegationsRaw(requestParameters: GetDelegationsRequest): Promise> { const queryParameters: any = {}; if (requestParameters.size !== undefined) { @@ -159,12 +159,12 @@ export class AccountsApi extends runtime.BaseAPI { query: queryParameters, }); - return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2004FromJSON(jsonValue)); + return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2005FromJSON(jsonValue)); } /** */ - async getDelegations(requestParameters: GetDelegationsRequest): Promise { + async getDelegations(requestParameters: GetDelegationsRequest): Promise { const response = await this.getDelegationsRaw(requestParameters); return await response.value(); } diff --git a/src/vendors/oasisscan/apis/OperationsEntityApi.ts b/src/vendors/oasisscan/apis/OperationsEntityApi.ts new file mode 100644 index 0000000000..3444c8151e --- /dev/null +++ b/src/vendors/oasisscan/apis/OperationsEntityApi.ts @@ -0,0 +1,62 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Oasisscan API + * https://github.com/bitcat365/oasisscan-backend#readme https://api.oasisscan.com/mainnet/swagger-ui/#/ https://api.oasisscan.com/mainnet/v2/api-docs + * + * The version of the OpenAPI document: 1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import * as runtime from '../runtime'; +import { + InlineResponse2002, + InlineResponse2002FromJSON, + InlineResponse2002ToJSON, +} from '../models'; + +export interface GetTransactionRequest { + hash: string; +} + +/** + * + */ +export class OperationsEntityApi extends runtime.BaseAPI { + + /** + * TransactionDetail + */ + async getTransactionRaw(requestParameters: GetTransactionRequest): Promise> { + if (requestParameters.hash === null || requestParameters.hash === undefined) { + throw new runtime.RequiredError('hash','Required parameter requestParameters.hash was null or undefined when calling getTransaction.'); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/chain/transaction/{hash}`.replace(`{${"hash"}}`, encodeURIComponent(String(requestParameters.hash))), + method: 'GET', + headers: headerParameters, + query: queryParameters, + }); + + return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2002FromJSON(jsonValue)); + } + + /** + * TransactionDetail + */ + async getTransaction(requestParameters: GetTransactionRequest): Promise { + const response = await this.getTransactionRaw(requestParameters); + return await response.value(); + } + +} diff --git a/src/vendors/oasisscan/apis/OperationsListApi.ts b/src/vendors/oasisscan/apis/OperationsListApi.ts index c0e1cfb79a..832f615d75 100644 --- a/src/vendors/oasisscan/apis/OperationsListApi.ts +++ b/src/vendors/oasisscan/apis/OperationsListApi.ts @@ -15,9 +15,9 @@ import * as runtime from '../runtime'; import { - InlineResponse2002, - InlineResponse2002FromJSON, - InlineResponse2002ToJSON, + InlineResponse2003, + InlineResponse2003FromJSON, + InlineResponse2003ToJSON, } from '../models'; export interface GetTransactionsListRequest { @@ -36,7 +36,7 @@ export class OperationsListApi extends runtime.BaseAPI { /** */ - async getTransactionsListRaw(requestParameters: GetTransactionsListRequest): Promise> { + async getTransactionsListRaw(requestParameters: GetTransactionsListRequest): Promise> { if (requestParameters.runtime === null || requestParameters.runtime === undefined) { throw new runtime.RequiredError('runtime','Required parameter requestParameters.runtime was null or undefined when calling getTransactionsList.'); } @@ -76,12 +76,12 @@ export class OperationsListApi extends runtime.BaseAPI { query: queryParameters, }); - return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2002FromJSON(jsonValue)); + return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2003FromJSON(jsonValue)); } /** */ - async getTransactionsList(requestParameters: GetTransactionsListRequest): Promise { + async getTransactionsList(requestParameters: GetTransactionsListRequest): Promise { const response = await this.getTransactionsListRaw(requestParameters); return await response.value(); } diff --git a/src/vendors/oasisscan/apis/RuntimeApi.ts b/src/vendors/oasisscan/apis/RuntimeApi.ts index e1f4f9840e..8cb2b456c6 100644 --- a/src/vendors/oasisscan/apis/RuntimeApi.ts +++ b/src/vendors/oasisscan/apis/RuntimeApi.ts @@ -15,9 +15,9 @@ import * as runtime from '../runtime'; import { - InlineResponse2003, - InlineResponse2003FromJSON, - InlineResponse2003ToJSON, + InlineResponse2004, + InlineResponse2004FromJSON, + InlineResponse2004ToJSON, } from '../models'; export interface GetRuntimeTransactionInfoRequest { @@ -33,7 +33,7 @@ export class RuntimeApi extends runtime.BaseAPI { /** */ - async getRuntimeTransactionInfoRaw(requestParameters: GetRuntimeTransactionInfoRequest): Promise> { + async getRuntimeTransactionInfoRaw(requestParameters: GetRuntimeTransactionInfoRequest): Promise> { if (requestParameters.id === null || requestParameters.id === undefined) { throw new runtime.RequiredError('id','Required parameter requestParameters.id was null or undefined when calling getRuntimeTransactionInfo.'); } @@ -65,12 +65,12 @@ export class RuntimeApi extends runtime.BaseAPI { query: queryParameters, }); - return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2003FromJSON(jsonValue)); + return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2004FromJSON(jsonValue)); } /** */ - async getRuntimeTransactionInfo(requestParameters: GetRuntimeTransactionInfoRequest): Promise { + async getRuntimeTransactionInfo(requestParameters: GetRuntimeTransactionInfoRequest): Promise { const response = await this.getRuntimeTransactionInfoRaw(requestParameters); return await response.value(); } diff --git a/src/vendors/oasisscan/apis/index.ts b/src/vendors/oasisscan/apis/index.ts index 21abe73b40..27bdb9f218 100644 --- a/src/vendors/oasisscan/apis/index.ts +++ b/src/vendors/oasisscan/apis/index.ts @@ -1,3 +1,4 @@ export * from './AccountsApi' +export * from './OperationsEntityApi' export * from './OperationsListApi' export * from './RuntimeApi' diff --git a/src/vendors/oasisscan/models/InlineResponse2002.ts b/src/vendors/oasisscan/models/InlineResponse2002.ts index 42ebf07b78..d5188003d2 100644 --- a/src/vendors/oasisscan/models/InlineResponse2002.ts +++ b/src/vendors/oasisscan/models/InlineResponse2002.ts @@ -14,10 +14,10 @@ import { exists, mapValues } from '../runtime'; import { - InlineResponse2002Data, - InlineResponse2002DataFromJSON, - InlineResponse2002DataFromJSONTyped, - InlineResponse2002DataToJSON, + OperationsEntity, + OperationsEntityFromJSON, + OperationsEntityFromJSONTyped, + OperationsEntityToJSON, } from './'; /** @@ -34,10 +34,10 @@ export interface InlineResponse2002 { code: number; /** * - * @type {InlineResponse2002Data} + * @type {OperationsEntity} * @memberof InlineResponse2002 */ - data: InlineResponse2002Data; + data: OperationsEntity; } export function InlineResponse2002FromJSON(json: any): InlineResponse2002 { @@ -51,7 +51,7 @@ export function InlineResponse2002FromJSONTyped(json: any, ignoreDiscriminator: return { 'code': json['code'], - 'data': InlineResponse2002DataFromJSON(json['data']), + 'data': OperationsEntityFromJSON(json['data']), }; } @@ -65,7 +65,7 @@ export function InlineResponse2002ToJSON(value?: InlineResponse2002 | null): any return { 'code': value.code, - 'data': InlineResponse2002DataToJSON(value.data), + 'data': OperationsEntityToJSON(value.data), }; } diff --git a/src/vendors/oasisscan/models/InlineResponse2003.ts b/src/vendors/oasisscan/models/InlineResponse2003.ts index c20fbe54c4..43a693d5c6 100644 --- a/src/vendors/oasisscan/models/InlineResponse2003.ts +++ b/src/vendors/oasisscan/models/InlineResponse2003.ts @@ -14,10 +14,10 @@ import { exists, mapValues } from '../runtime'; import { - RuntimeTransactionInfoRow, - RuntimeTransactionInfoRowFromJSON, - RuntimeTransactionInfoRowFromJSONTyped, - RuntimeTransactionInfoRowToJSON, + InlineResponse2003Data, + InlineResponse2003DataFromJSON, + InlineResponse2003DataFromJSONTyped, + InlineResponse2003DataToJSON, } from './'; /** @@ -34,10 +34,10 @@ export interface InlineResponse2003 { code: number; /** * - * @type {RuntimeTransactionInfoRow} + * @type {InlineResponse2003Data} * @memberof InlineResponse2003 */ - data: RuntimeTransactionInfoRow; + data: InlineResponse2003Data; } export function InlineResponse2003FromJSON(json: any): InlineResponse2003 { @@ -51,7 +51,7 @@ export function InlineResponse2003FromJSONTyped(json: any, ignoreDiscriminator: return { 'code': json['code'], - 'data': json['data'], + 'data': InlineResponse2003DataFromJSON(json['data']), }; } @@ -65,7 +65,7 @@ export function InlineResponse2003ToJSON(value?: InlineResponse2003 | null): any return { 'code': value.code, - 'data': value.data, + 'data': InlineResponse2003DataToJSON(value.data), }; } diff --git a/src/vendors/oasisscan/models/InlineResponse2003Data.ts b/src/vendors/oasisscan/models/InlineResponse2003Data.ts index 1f99da7c7b..a1c0a93480 100644 --- a/src/vendors/oasisscan/models/InlineResponse2003Data.ts +++ b/src/vendors/oasisscan/models/InlineResponse2003Data.ts @@ -2,7 +2,7 @@ /* eslint-disable */ /** * Oasisscan API - * https://github.com/bitcat365/oasisscan-backend#readme + * https://github.com/bitcat365/oasisscan-backend#readme https://api.oasisscan.com/mainnet/swagger-ui/#/ https://api.oasisscan.com/mainnet/v2/api-docs * * The version of the OpenAPI document: 1 * @@ -14,10 +14,10 @@ import { exists, mapValues } from '../runtime'; import { - ParaTimeCtxRow, - ParaTimeCtxRowFromJSON, - ParaTimeCtxRowFromJSONTyped, - ParaTimeCtxRowToJSON, + OperationsRow, + OperationsRowFromJSON, + OperationsRowFromJSONTyped, + OperationsRowToJSON, } from './'; /** @@ -28,46 +28,34 @@ import { export interface InlineResponse2003Data { /** * - * @type {ParaTimeCtxRow} + * @type {Array} * @memberof InlineResponse2003Data */ - ctx: ParaTimeCtxRow; - /** - * - * @type {string} - * @memberof InlineResponse2003Data - */ - runtimeName: string; - /** - * - * @type {string} - * @memberof InlineResponse2003Data - */ - runtimeId: string; + list: Array; /** * * @type {number} * @memberof InlineResponse2003Data */ - round: number; + page: number; /** * * @type {number} * @memberof InlineResponse2003Data */ - timestamp?: number; + size: number; /** * - * @type {string} + * @type {number} * @memberof InlineResponse2003Data */ - txHash?: string; + maxPage: number; /** * - * @type {boolean} + * @type {number} * @memberof InlineResponse2003Data */ - result?: boolean; + totalSize: number; } export function InlineResponse2003DataFromJSON(json: any): InlineResponse2003Data { @@ -80,13 +68,11 @@ export function InlineResponse2003DataFromJSONTyped(json: any, ignoreDiscriminat } return { - 'ctx': ParaTimeCtxRowFromJSON(json['ctx']), - 'runtimeName': json['runtimeName'], - 'runtimeId': json['runtimeId'], - 'round': json['round'], - 'timestamp': !exists(json, 'timestamp') ? undefined : json['timestamp'], - 'txHash': !exists(json, 'txHash') ? undefined : json['txHash'], - 'result': !exists(json, 'result') ? undefined : json['result'], + 'list': ((json['list'] as Array).map(OperationsRowFromJSON)), + 'page': json['page'], + 'size': json['size'], + 'maxPage': json['maxPage'], + 'totalSize': json['totalSize'], }; } @@ -99,13 +85,11 @@ export function InlineResponse2003DataToJSON(value?: InlineResponse2003Data | nu } return { - 'ctx': ParaTimeCtxRowToJSON(value.ctx), - 'runtimeName': value.runtimeName, - 'runtimeId': value.runtimeId, - 'round': value.round, - 'timestamp': value.timestamp, - 'txHash': value.txHash, - 'result': value.result, + 'list': ((value.list as Array).map(OperationsRowToJSON)), + 'page': value.page, + 'size': value.size, + 'maxPage': value.maxPage, + 'totalSize': value.totalSize, }; } diff --git a/src/vendors/oasisscan/models/InlineResponse2004.ts b/src/vendors/oasisscan/models/InlineResponse2004.ts index 89548cba12..f9c5cc44a1 100644 --- a/src/vendors/oasisscan/models/InlineResponse2004.ts +++ b/src/vendors/oasisscan/models/InlineResponse2004.ts @@ -14,10 +14,10 @@ import { exists, mapValues } from '../runtime'; import { - InlineResponse2004Data, - InlineResponse2004DataFromJSON, - InlineResponse2004DataFromJSONTyped, - InlineResponse2004DataToJSON, + RuntimeTransactionInfoRow, + RuntimeTransactionInfoRowFromJSON, + RuntimeTransactionInfoRowFromJSONTyped, + RuntimeTransactionInfoRowToJSON, } from './'; /** @@ -34,10 +34,10 @@ export interface InlineResponse2004 { code: number; /** * - * @type {InlineResponse2004Data} + * @type {RuntimeTransactionInfoRow} * @memberof InlineResponse2004 */ - data: InlineResponse2004Data; + data: RuntimeTransactionInfoRow; } export function InlineResponse2004FromJSON(json: any): InlineResponse2004 { @@ -51,7 +51,7 @@ export function InlineResponse2004FromJSONTyped(json: any, ignoreDiscriminator: return { 'code': json['code'], - 'data': InlineResponse2004DataFromJSON(json['data']), + 'data': json['data'], }; } @@ -65,7 +65,7 @@ export function InlineResponse2004ToJSON(value?: InlineResponse2004 | null): any return { 'code': value.code, - 'data': InlineResponse2004DataToJSON(value.data), + 'data': value.data, }; } diff --git a/src/vendors/oasisscan/models/InlineResponse2005Data.ts b/src/vendors/oasisscan/models/InlineResponse2005Data.ts index 73723153b9..3f07379568 100644 --- a/src/vendors/oasisscan/models/InlineResponse2005Data.ts +++ b/src/vendors/oasisscan/models/InlineResponse2005Data.ts @@ -14,10 +14,10 @@ import { exists, mapValues } from '../runtime'; import { - DebondingDelegationRow, - DebondingDelegationRowFromJSON, - DebondingDelegationRowFromJSONTyped, - DebondingDelegationRowToJSON, + DelegationRow, + DelegationRowFromJSON, + DelegationRowFromJSONTyped, + DelegationRowToJSON, } from './'; /** @@ -28,10 +28,10 @@ import { export interface InlineResponse2005Data { /** * - * @type {Array} + * @type {Array} * @memberof InlineResponse2005Data */ - list: Array; + list: Array; /** * * @type {number} @@ -68,7 +68,7 @@ export function InlineResponse2005DataFromJSONTyped(json: any, ignoreDiscriminat } return { - 'list': ((json['list'] as Array).map(DebondingDelegationRowFromJSON)), + 'list': ((json['list'] as Array).map(DelegationRowFromJSON)), 'page': json['page'], 'size': json['size'], 'maxPage': json['maxPage'], @@ -85,7 +85,7 @@ export function InlineResponse2005DataToJSON(value?: InlineResponse2005Data | nu } return { - 'list': ((value.list as Array).map(DebondingDelegationRowToJSON)), + 'list': ((value.list as Array).map(DelegationRowToJSON)), 'page': value.page, 'size': value.size, 'maxPage': value.maxPage, diff --git a/src/vendors/oasisscan/models/InlineResponse2006.ts b/src/vendors/oasisscan/models/InlineResponse2006.ts new file mode 100644 index 0000000000..939b0b6448 --- /dev/null +++ b/src/vendors/oasisscan/models/InlineResponse2006.ts @@ -0,0 +1,72 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Oasisscan API + * https://github.com/bitcat365/oasisscan-backend#readme https://api.oasisscan.com/mainnet/swagger-ui/#/ https://api.oasisscan.com/mainnet/v2/api-docs + * + * The version of the OpenAPI document: 1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +import { + InlineResponse2006Data, + InlineResponse2006DataFromJSON, + InlineResponse2006DataFromJSONTyped, + InlineResponse2006DataToJSON, +} from './'; + +/** + * + * @export + * @interface InlineResponse2006 + */ +export interface InlineResponse2006 { + /** + * + * @type {number} + * @memberof InlineResponse2006 + */ + code: number; + /** + * + * @type {InlineResponse2006Data} + * @memberof InlineResponse2006 + */ + data: InlineResponse2006Data; +} + +export function InlineResponse2006FromJSON(json: any): InlineResponse2006 { + return InlineResponse2006FromJSONTyped(json, false); +} + +export function InlineResponse2006FromJSONTyped(json: any, ignoreDiscriminator: boolean): InlineResponse2006 { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'code': json['code'], + 'data': InlineResponse2006DataFromJSON(json['data']), + }; +} + +export function InlineResponse2006ToJSON(value?: InlineResponse2006 | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'code': value.code, + 'data': InlineResponse2006DataToJSON(value.data), + }; +} + + diff --git a/src/vendors/oasisscan/models/InlineResponse2006Data.ts b/src/vendors/oasisscan/models/InlineResponse2006Data.ts new file mode 100644 index 0000000000..8f752b7d29 --- /dev/null +++ b/src/vendors/oasisscan/models/InlineResponse2006Data.ts @@ -0,0 +1,96 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Oasisscan API + * https://github.com/bitcat365/oasisscan-backend#readme https://api.oasisscan.com/mainnet/swagger-ui/#/ https://api.oasisscan.com/mainnet/v2/api-docs + * + * The version of the OpenAPI document: 1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +import { + DebondingDelegationRow, + DebondingDelegationRowFromJSON, + DebondingDelegationRowFromJSONTyped, + DebondingDelegationRowToJSON, +} from './'; + +/** + * + * @export + * @interface InlineResponse2006Data + */ +export interface InlineResponse2006Data { + /** + * + * @type {Array} + * @memberof InlineResponse2006Data + */ + list: Array; + /** + * + * @type {number} + * @memberof InlineResponse2006Data + */ + page: number; + /** + * + * @type {number} + * @memberof InlineResponse2006Data + */ + size: number; + /** + * + * @type {number} + * @memberof InlineResponse2006Data + */ + maxPage: number; + /** + * + * @type {number} + * @memberof InlineResponse2006Data + */ + totalSize: number; +} + +export function InlineResponse2006DataFromJSON(json: any): InlineResponse2006Data { + return InlineResponse2006DataFromJSONTyped(json, false); +} + +export function InlineResponse2006DataFromJSONTyped(json: any, ignoreDiscriminator: boolean): InlineResponse2006Data { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'list': ((json['list'] as Array).map(DebondingDelegationRowFromJSON)), + 'page': json['page'], + 'size': json['size'], + 'maxPage': json['maxPage'], + 'totalSize': json['totalSize'], + }; +} + +export function InlineResponse2006DataToJSON(value?: InlineResponse2006Data | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'list': ((value.list as Array).map(DebondingDelegationRowToJSON)), + 'page': value.page, + 'size': value.size, + 'maxPage': value.maxPage, + 'totalSize': value.totalSize, + }; +} + + diff --git a/src/vendors/oasisscan/models/OperationsEntity.ts b/src/vendors/oasisscan/models/OperationsEntity.ts new file mode 100644 index 0000000000..ab7158a0c6 --- /dev/null +++ b/src/vendors/oasisscan/models/OperationsEntity.ts @@ -0,0 +1,181 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Oasisscan API + * https://github.com/bitcat365/oasisscan-backend#readme https://api.oasisscan.com/mainnet/swagger-ui/#/ https://api.oasisscan.com/mainnet/v2/api-docs + * + * The version of the OpenAPI document: 1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * + * @export + * @interface OperationsEntity + */ +export interface OperationsEntity { + /** + * + * @type {string} + * @memberof OperationsEntity + */ + txHash: string; + /** + * + * @type {number} + * @memberof OperationsEntity + */ + height: number; + /** + * + * @type {string} + * @memberof OperationsEntity + */ + method: OperationsEntityMethodEnum; + /** + * + * @type {string} + * @memberof OperationsEntity + */ + fee: string; + /** + * + * @type {string} + * @memberof OperationsEntity + */ + amount: string | null; + /** + * + * @type {string} + * @memberof OperationsEntity + */ + shares?: string | null; + /** + * + * @type {number} + * @memberof OperationsEntity + */ + timestamp: number; + /** + * + * @type {number} + * @memberof OperationsEntity + */ + time: number; + /** + * + * @type {boolean} + * @memberof OperationsEntity + */ + status: boolean; + /** + * + * @type {string} + * @memberof OperationsEntity + */ + from: string; + /** + * + * @type {string} + * @memberof OperationsEntity + */ + to: string | null; + /** + * + * @type {string} + * @memberof OperationsEntity + */ + errorMessage?: string | null; + /** + * + * @type {number} + * @memberof OperationsEntity + */ + nonce: number; +} + +/** +* @export +* @enum {string} +*/ +export enum OperationsEntityMethodEnum { + StakingTransfer = 'staking.Transfer', + StakingAddEscrow = 'staking.AddEscrow', + StakingReclaimEscrow = 'staking.ReclaimEscrow', + StakingAmendCommissionSchedule = 'staking.AmendCommissionSchedule', + StakingAllow = 'staking.Allow', + StakingWithdraw = 'staking.Withdraw', + StakingBurn = 'staking.Burn', + RoothashExecutorCommit = 'roothash.ExecutorCommit', + RoothashExecutorProposerTimeout = 'roothash.ExecutorProposerTimeout', + RoothashSubmitMsg = 'roothash.SubmitMsg', + RegistryDeregisterEntity = 'registry.DeregisterEntity', + RegistryRegisterEntity = 'registry.RegisterEntity', + RegistryRegisterNode = 'registry.RegisterNode', + RegistryRegisterRuntime = 'registry.RegisterRuntime', + RegistryUnfreezeNode = 'registry.UnfreezeNode', + GovernanceCastVote = 'governance.CastVote', + GovernanceSubmitProposal = 'governance.SubmitProposal', + BeaconPvssCommit = 'beacon.PVSSCommit', + BeaconPvssReveal = 'beacon.PVSSReveal', + BeaconVrfProve = 'beacon.VRFProve', + ConsensusMeta = 'consensus.Meta' +} + +export function OperationsEntityFromJSON(json: any): OperationsEntity { + return OperationsEntityFromJSONTyped(json, false); +} + +export function OperationsEntityFromJSONTyped(json: any, ignoreDiscriminator: boolean): OperationsEntity { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'txHash': json['txHash'], + 'height': json['height'], + 'method': json['method'], + 'fee': json['fee'], + 'amount': json['amount'], + 'shares': !exists(json, 'shares') ? undefined : json['shares'], + 'timestamp': json['timestamp'], + 'time': json['time'], + 'status': json['status'], + 'from': json['from'], + 'to': json['to'], + 'errorMessage': !exists(json, 'errorMessage') ? undefined : json['errorMessage'], + 'nonce': json['nonce'], + }; +} + +export function OperationsEntityToJSON(value?: OperationsEntity | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'txHash': value.txHash, + 'height': value.height, + 'method': value.method, + 'fee': value.fee, + 'amount': value.amount, + 'shares': value.shares, + 'timestamp': value.timestamp, + 'time': value.time, + 'status': value.status, + 'from': value.from, + 'to': value.to, + 'errorMessage': value.errorMessage, + 'nonce': value.nonce, + }; +} + + diff --git a/src/vendors/oasisscan/models/index.ts b/src/vendors/oasisscan/models/index.ts index 8d8a0b17f7..2654186f6b 100644 --- a/src/vendors/oasisscan/models/index.ts +++ b/src/vendors/oasisscan/models/index.ts @@ -6,12 +6,14 @@ export * from './InlineResponse200' export * from './InlineResponse2001' export * from './InlineResponse2001Data' export * from './InlineResponse2002' -export * from './InlineResponse2002Data' export * from './InlineResponse2003' +export * from './InlineResponse2003Data' export * from './InlineResponse2004' -export * from './InlineResponse2004Data' export * from './InlineResponse2005' export * from './InlineResponse2005Data' +export * from './InlineResponse2006' +export * from './InlineResponse2006Data' +export * from './OperationsEntity' export * from './OperationsRow' export * from './ParaTimeCtxRow' export * from './RuntimeTransactionInfoRow' diff --git a/src/vendors/oasisscan/swagger.yml b/src/vendors/oasisscan/swagger.yml index 46df381e89..7b7aea3a5f 100644 --- a/src/vendors/oasisscan/swagger.yml +++ b/src/vendors/oasisscan/swagger.yml @@ -68,6 +68,32 @@ paths: type: array items: $ref: '#/components/schemas/ValidatorRow' + '/chain/transaction/{hash}': + get: + tags: + - OperationsEntity + summary: TransactionDetail + operationId: getTransaction + parameters: + - name: hash + in: path + description: hash + required: true + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + required: [ 'code', 'data' ] + properties: + code: + type: integer + data: + "$ref": '#/components/schemas/OperationsEntity' '/chain/transactions': get: tags: @@ -494,6 +520,73 @@ components: - status - from - to + OperationsEntity: + type: object + properties: + txHash: + type: string + height: + type: integer + method: + type: string + enum: + - staking.Transfer + - staking.AddEscrow + - staking.ReclaimEscrow + - staking.AmendCommissionSchedule + - staking.Allow + - staking.Withdraw + - staking.Burn + - roothash.ExecutorCommit + - roothash.ExecutorProposerTimeout + - roothash.SubmitMsg + - registry.DeregisterEntity + - registry.RegisterEntity + - registry.RegisterNode + - registry.RegisterRuntime + - registry.UnfreezeNode + - governance.CastVote + - governance.SubmitProposal + - beacon.PVSSCommit + - beacon.PVSSReveal + - beacon.VRFProve + - consensus.Meta + fee: + type: string + amount: + type: string + nullable: true + shares: + type: string + nullable: true + timestamp: + type: integer + time: + type: integer + status: + type: boolean + from: + type: string + to: + type: string + nullable: true + errorMessage: + type: string + nullable: true + nonce: + type: integer + required: + - timestamp + - txHash + - height + - method + - fee + - amount + - time + - status + - from + - to + - nonce DelegationRow: type: object properties: From 1e123da5ef586a202460cb8dae285bd9d3ef7b51 Mon Sep 17 00:00:00 2001 From: Matej Lubej Date: Mon, 27 May 2024 08:22:59 +0200 Subject: [PATCH 02/12] Pending transactions --- .../Transaction/__tests__/index.test.tsx | 2 + src/app/components/Transaction/index.tsx | 36 +++++++--- src/app/lib/getAccountBalanceWithFallback.ts | 1 + .../Features/TransactionHistory/index.tsx | 70 +++++++++++++++++-- src/app/state/account/index.ts | 40 +++++++++-- src/app/state/account/saga.ts | 29 +++++++- src/app/state/account/selectors.ts | 42 +++++++++++ src/app/state/account/types.ts | 14 ++++ src/app/state/persist/syncTabs.ts | 1 + src/app/state/transaction/saga.ts | 42 ++++++++++- src/app/state/transaction/types.ts | 2 + src/config.ts | 4 ++ src/locales/en/translation.json | 7 +- src/utils/__fixtures__/test-inputs.ts | 12 ++++ src/vendors/oasisscan.ts | 2 +- 15 files changed, 277 insertions(+), 27 deletions(-) diff --git a/src/app/components/Transaction/__tests__/index.test.tsx b/src/app/components/Transaction/__tests__/index.test.tsx index a7b22e621e..0d7e7ffbcc 100644 --- a/src/app/components/Transaction/__tests__/index.test.tsx +++ b/src/app/components/Transaction/__tests__/index.test.tsx @@ -108,6 +108,7 @@ describe('', () => { runtimeName: undefined, runtimeId: undefined, round: undefined, + nonce: 0n.toString(), }, network, ) @@ -131,6 +132,7 @@ describe('', () => { runtimeName: undefined, runtimeId: undefined, round: undefined, + nonce: 0n.toString(), }, network, ) diff --git a/src/app/components/Transaction/index.tsx b/src/app/components/Transaction/index.tsx index 51951dc6e2..74aa6249d2 100644 --- a/src/app/components/Transaction/index.tsx +++ b/src/app/components/Transaction/index.tsx @@ -462,9 +462,11 @@ export function Transaction(props: TransactionProps) { - - {intlDateTimeFormat(transaction.timestamp!)} - + {transaction.timestamp && ( + + {intlDateTimeFormat(transaction.timestamp)} + + )} {!transaction.runtimeId && transaction.level && ( @@ -486,15 +488,31 @@ export function Transaction(props: TransactionProps) { { + switch (transaction.status) { + case TransactionStatus.Successful: + return 'successful-label' + case TransactionStatus.Pending: + return 'status-warning' + case TransactionStatus.Failed: + default: + return 'status-error' + } + })()} size="small" weight="bold" > - {transaction.status === TransactionStatus.Successful ? ( - {t('account.transaction.successful', 'Successful')} - ) : ( - {t('account.transaction.failed', 'Failed')} - )} + {(() => { + switch (transaction.status) { + case TransactionStatus.Successful: + return {t('account.transaction.successful', 'Successful')} + case TransactionStatus.Pending: + return {t('account.transaction.pending', 'Pending')} + case TransactionStatus.Failed: + default: + return {t('account.transaction.failed', 'Failed')} + } + })()} diff --git a/src/app/lib/getAccountBalanceWithFallback.ts b/src/app/lib/getAccountBalanceWithFallback.ts index 1c5d03115f..5c73e8e539 100644 --- a/src/app/lib/getAccountBalanceWithFallback.ts +++ b/src/app/lib/getAccountBalanceWithFallback.ts @@ -14,6 +14,7 @@ function* getBalanceGRPC(address: string) { delegations: null, debonding: null, total: null, + nonce: account.general?.nonce?.toString() ?? '0', } } diff --git a/src/app/pages/AccountPage/Features/TransactionHistory/index.tsx b/src/app/pages/AccountPage/Features/TransactionHistory/index.tsx index 6c30a309a4..698179cdc6 100644 --- a/src/app/pages/AccountPage/Features/TransactionHistory/index.tsx +++ b/src/app/pages/AccountPage/Features/TransactionHistory/index.tsx @@ -11,36 +11,94 @@ import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { + hasAccountUnknownPendingTransactions, selectAccountAddress, + selectPendingTransactionForAccount, selectTransactions, selectTransactionsError, } from 'app/state/account/selectors' import { selectSelectedNetwork } from 'app/state/network/selectors' import { ErrorFormatter } from 'app/components/ErrorFormatter' - -interface Props {} +import { AlertBox } from '../../../../components/AlertBox' +import { Anchor } from 'grommet/es6/components/Anchor' +import { Text } from 'grommet/es6/components/Text' +import { FormNext } from 'grommet-icons/es6/icons/FormNext' +import { config } from '../../../../../config' +import { backend } from '../../../../../vendors/backend' /** * Displays the past transactions from the state for a given account */ -export function TransactionHistory(props: Props) { +export function TransactionHistory() { const { t } = useTranslation() const allTransactions = useSelector(selectTransactions) const transactionsError = useSelector(selectTransactionsError) const address = useSelector(selectAccountAddress) + const pendingTransactions = useSelector(selectPendingTransactionForAccount) + const hasUnknownPendingTransactions = useSelector(hasAccountUnknownPendingTransactions) const network = useSelector(selectSelectedNetwork) - const transactionComponents = allTransactions.map((t, i) => ( - + const backendLinks = config[network][backend()] + const transactionComponents = allTransactions.map(t => ( + )) + const pendingTransactionComponents = pendingTransactions + .filter(({ hash: pendingTxHash }) => !allTransactions.some(({ hash }) => hash === pendingTxHash)) + .map(t => ) return ( {transactionsError && (

- {t('account.transaction.loadingError', "Couldn't load transactions.")}{' '} + {t('account.transaction.loadingError', `Couldn't load transactions.`)}{' '}

)} + {(!!pendingTransactionComponents.length || hasUnknownPendingTransactions) && ( + <> + {t('account.summary.pendingTransactions', 'Pending transactions')} + + {backendLinks.blockExplorerAccount && ( + + + {t('account.summary.viewAccountTxs', 'View account transactions')} + + + + )} + + } + > + + {t( + 'account.summary.someTxsInPendingState', + 'Some transactions are currently in a pending state.', + )} + + + + {pendingTransactionComponents.length ? ( + // eslint-disable-next-line no-restricted-syntax -- pendingTransactionComponents is not a plain text node + pendingTransactionComponents + ) : ( + <> + )} + + {t('account.summary.activity', 'Activity')} + + )} {allTransactions.length ? ( // eslint-disable-next-line no-restricted-syntax -- transactionComponents is not a plain text node transactionComponents diff --git a/src/app/state/account/index.ts b/src/app/state/account/index.ts index 3f6fe82ae1..3522076559 100644 --- a/src/app/state/account/index.ts +++ b/src/app/state/account/index.ts @@ -1,8 +1,7 @@ import { PayloadAction } from '@reduxjs/toolkit' -import { Transaction } from 'app/state/transaction/types' import { ErrorPayload } from 'types/errors' import { createSlice } from 'utils/@reduxjs/toolkit' -import { AccountState, Account } from './types' +import { AccountState, Account, PendingTransactionPayload, TransactionsLoadedPayload } from './types' export const initialState: AccountState = { address: '', @@ -15,6 +14,12 @@ export const initialState: AccountState = { transactions: [], transactionsError: undefined, loading: true, + pendingTransactions: { + local: [], + testnet: [], + mainnet: [], + }, + nonce: '0', } export const accountSlice = createSlice({ @@ -23,7 +28,7 @@ export const accountSlice = createSlice({ reducers: { openAccountPage(state, action: PayloadAction) {}, closeAccountPage(state) {}, - clearAccount(state, action: PayloadAction) { + clearAccount(state, action: PayloadAction) { Object.assign(state, initialState) }, fetchAccount(state, action: PayloadAction) {}, @@ -34,9 +39,20 @@ export const accountSlice = createSlice({ accountError(state, action: PayloadAction) { state.accountError = action.payload }, - transactionsLoaded(state, action: PayloadAction) { + transactionsLoaded(state, action: PayloadAction) { + const { + payload: { networkType, transactions }, + } = action + state.transactionsError = undefined - state.transactions = action.payload + state.transactions = transactions + + state.pendingTransactions = { + ...state.pendingTransactions, + [networkType]: state.pendingTransactions[networkType].filter( + ({ hash: pendingTxHash }) => !transactions.some(({ hash }) => pendingTxHash === hash), + ), + } }, transactionsError(state, action: PayloadAction) { state.transactionsError = action.payload @@ -46,6 +62,20 @@ export const accountSlice = createSlice({ setLoading(state, action: PayloadAction) { state.loading = action.payload }, + addPendingTransaction(state, action: PayloadAction) { + const { + payload: { from, networkType, transaction }, + } = action + + if (from !== state.address) { + return + } + + state.pendingTransactions = { + ...state.pendingTransactions, + [networkType]: [transaction, ...state.pendingTransactions[networkType]], + } + }, }, }) diff --git a/src/app/state/account/saga.ts b/src/app/state/account/saga.ts index 53126ba466..26f1be73ea 100644 --- a/src/app/state/account/saga.ts +++ b/src/app/state/account/saga.ts @@ -13,6 +13,8 @@ import { selectAddress } from '../wallet/selectors' import { selectAccountAddress, selectAccount } from './selectors' import { getAccountBalanceWithFallback } from '../../lib/getAccountBalanceWithFallback' import { walletActions } from '../wallet' +import { selectSelectedNetwork } from '../network/selectors' +import { Transaction } from '../transaction/types' const ACCOUNT_REFETCHING_INTERVAL = process.env.REACT_APP_E2E_TEST ? 5 * 1000 : 30 * 1000 const TRANSACTIONS_UPDATE_DELAY = 35 * 1000 // Measured between 8 and 31 second additional delay after balance updates @@ -22,7 +24,7 @@ export function* fetchAccount(action: PayloadAction) { const address = action.payload yield* put(accountActions.setLoading(true)) - const { getTransactionsList } = yield* call(getExplorerAPIs) + const { getTransactionsList, getTransaction } = yield* call(getExplorerAPIs) yield* all([ join( @@ -47,12 +49,33 @@ export function* fetchAccount(action: PayloadAction) { ), join( yield* fork(function* () { + const networkType = yield* select(selectSelectedNetwork) + try { - const transactions = yield* call(getTransactionsList, { + const transactions: Transaction[] = yield* call(getTransactionsList, { accountId: address, limit: TRANSACTIONS_LIMIT, }) - yield* put(accountActions.transactionsLoaded(transactions)) + + const detailedTransactions = yield* call(() => + Promise.allSettled(transactions.map(({ hash }) => getTransaction({ hash }))), + ) + const transactionsWithDetails = transactions.map((t, i) => { + const { status, value } = detailedTransactions[i] as PromiseFulfilledResult + // Skip failed txs + if (status === 'fulfilled') { + return { + ...t, + ...value, + } + } + + return t + }) + + yield* put( + accountActions.transactionsLoaded({ networkType, transactions: transactionsWithDetails }), + ) } catch (e: any) { console.error('get transactions list failed, continuing without updated list.', e) if (e instanceof WalletError) { diff --git a/src/app/state/account/selectors.ts b/src/app/state/account/selectors.ts index 880604077a..9bbde4e7c4 100644 --- a/src/app/state/account/selectors.ts +++ b/src/app/state/account/selectors.ts @@ -2,6 +2,7 @@ import { createSelector } from '@reduxjs/toolkit' import { RootState } from 'types' import { initialState } from '.' +import { selectSelectedNetwork } from '../network/selectors' const selectSlice = (state: RootState) => state.account || initialState @@ -12,3 +13,44 @@ export const selectAccountAddress = createSelector([selectAccount], account => a export const selectAccountAvailableBalance = createSelector([selectAccount], account => account.available) export const selectAccountIsLoading = createSelector([selectAccount], account => account.loading) export const selectAccountAllowances = createSelector([selectAccount], account => account.allowances) +export const selectPendingTransactions = createSelector( + [selectAccount], + account => account.pendingTransactions, +) +export const selectAccountNonce = createSelector([selectAccount], account => account.nonce ?? '0') +export const selectPendingTransactionForAccount = createSelector( + [selectPendingTransactions, selectSelectedNetwork], + (pendingTransactions, networkType) => pendingTransactions[networkType] ?? [], +) +export const hasAccountUnknownPendingTransactions = createSelector( + [selectAccountNonce, selectTransactions, selectAccountAddress], + (accountNonce, transactions, accountAddress) => { + // TODO: This logic fails, in case transaction are not from the initial page of account's transactions + const filteredTransactions = transactions.filter(({ from }) => from && from === accountAddress) + + if (filteredTransactions.length === 0 && BigInt(accountNonce) === 0n) return false + + const maxNonceFromTxs = filteredTransactions.reduce( + (acc, { nonce }) => { + if (!nonce) { + return acc + } + + const bigNonce = BigInt(nonce) + + if (!acc) { + return bigNonce + } + + return bigNonce > acc ? bigNonce : acc + }, + undefined as bigint | undefined, + ) + + if (!maxNonceFromTxs) { + return BigInt(accountNonce) > 0n + } + + return BigInt(maxNonceFromTxs) + BigInt(1) < BigInt(accountNonce) + }, +) diff --git a/src/app/state/account/types.ts b/src/app/state/account/types.ts index fa6132001e..15a2f20ef9 100644 --- a/src/app/state/account/types.ts +++ b/src/app/state/account/types.ts @@ -1,6 +1,7 @@ import { Transaction } from 'app/state/transaction/types' import { ErrorPayload } from 'types/errors' import { StringifiedBigInt } from 'types/StringifiedBigInt' +import { NetworkType } from '../network/types' export interface BalanceDetails { available: StringifiedBigInt | null @@ -20,6 +21,7 @@ export interface Allowance { export interface Account extends BalanceDetails { address: string allowances?: Allowance[] + nonce: StringifiedBigInt } /* --- STATE --- */ @@ -28,4 +30,16 @@ export interface AccountState extends Account { accountError?: ErrorPayload transactions: Transaction[] transactionsError?: ErrorPayload + pendingTransactions: Record +} + +export interface PendingTransactionPayload { + from: string + networkType: NetworkType + transaction: Transaction +} + +export interface TransactionsLoadedPayload { + transactions: Transaction[] + networkType: NetworkType } diff --git a/src/app/state/persist/syncTabs.ts b/src/app/state/persist/syncTabs.ts index edf7cacc8d..9e28ebd31b 100644 --- a/src/app/state/persist/syncTabs.ts +++ b/src/app/state/persist/syncTabs.ts @@ -79,6 +79,7 @@ export const whitelistTabSyncActions: Record = { [rootSlices.account.actions.setLoading.type]: false, [rootSlices.account.actions.transactionsError.type]: false, [rootSlices.account.actions.transactionsLoaded.type]: false, + [rootSlices.account.actions.addPendingTransaction.type]: false, [rootSlices.createWallet.actions.clear.type]: false, [rootSlices.createWallet.actions.generateMnemonic.type]: false, [rootSlices.createWallet.actions.setChecked.type]: false, diff --git a/src/app/state/transaction/saga.ts b/src/app/state/transaction/saga.ts index 8b10ccf824..3723695950 100644 --- a/src/app/state/transaction/saga.ts +++ b/src/app/state/transaction/saga.ts @@ -11,11 +11,12 @@ import { transactionActions } from '.' import { sign } from '../importaccounts/saga' import { getOasisNic } from '../network/saga' import { selectAccountAddress, selectAccountAllowances } from '../account/selectors' -import { selectChainContext } from '../network/selectors' +import { selectChainContext, selectSelectedNetwork } from '../network/selectors' import { selectActiveWallet } from '../wallet/selectors' import { Wallet, WalletType } from '../wallet/types' -import { TransactionPayload, TransactionStep } from './types' +import { Transaction, TransactionPayload, TransactionStatus, TransactionStep, TransactionType } from './types' import { ParaTimeTransaction, Runtime, TransactionTypes } from '../paratimes/types' +import { accountActions } from '../account' export function* transactionSaga() { yield* takeEvery(transactionActions.sendTransaction, doTransaction) @@ -137,6 +138,7 @@ export function* doTransaction(action: PayloadAction) { const wallet = yield* select(selectActiveWallet) const nic = yield* call(getOasisNic) const chainContext = yield* select(selectChainContext) + const networkType = yield* select(selectSelectedNetwork) try { yield* setStep(TransactionStep.Building) @@ -201,6 +203,42 @@ export function* doTransaction(action: PayloadAction) { // Notify that the transaction was a success yield* put(transactionActions.transactionSent(action.payload)) + + const hash = yield* call([tw, tw.hash]) + + const transaction: Transaction = { + hash, + type: tw.transaction.method as TransactionType, + from: activeWallet.address, + amount: action.payload.amount, + to: undefined, + ...(action.payload.type === 'transfer' + ? { + to: action.payload.to, + } + : {}), + ...(action.payload.type === 'addEscrow' + ? { + to: action.payload.validator, + } + : {}), + ...(action.payload.type === 'reclaimEscrow' + ? { + to: action.payload.validator, + } + : {}), + status: TransactionStatus.Pending, + fee: undefined, + level: undefined, + round: undefined, + runtimeId: undefined, + runtimeName: undefined, + timestamp: undefined, + nonce: undefined, + } + + // TODO: Handle ParaTime transactions in similar way + yield* put(accountActions.addPendingTransaction({ transaction, from: activeWallet.address, networkType })) } catch (e: any) { let payload: ErrorPayload if (e instanceof WalletError) { diff --git a/src/app/state/transaction/types.ts b/src/app/state/transaction/types.ts index 01bfb9dbeb..e10bd512b6 100644 --- a/src/app/state/transaction/types.ts +++ b/src/app/state/transaction/types.ts @@ -36,6 +36,7 @@ export enum TransactionType { export enum TransactionStatus { Failed, Successful, + Pending, } export interface Transaction { @@ -53,6 +54,7 @@ export interface Transaction { runtimeName: string | undefined runtimeId: string | undefined round: number | undefined + nonce: StringifiedBigInt | undefined } /* --- STATE --- */ diff --git a/src/config.ts b/src/config.ts index 6ed5df1332..48f0eaba1b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -12,6 +12,7 @@ type BackendApiUrls = { explorer: string blockExplorer: string blockExplorerParatimes?: string + blockExplorerAccount?: string } type BackendProviders = { @@ -39,6 +40,7 @@ export const config: BackendConfig = { explorer: 'https://api.oasisscan.com/mainnet', blockExplorer: 'https://oasisscan.com/transactions/{{txHash}}', blockExplorerParatimes: 'https://oasisscan.com/paratimes/transactions/{{txHash}}?runtime={{runtimeId}}', + blockExplorerAccount: 'https://www.oasisscan.com/accounts/detail/{{address}}', }, }, testnet: { @@ -54,6 +56,7 @@ export const config: BackendConfig = { blockExplorer: 'https://testnet.oasisscan.com/transactions/{{txHash}}', blockExplorerParatimes: 'https://testnet.oasisscan.com/paratimes/transactions/{{txHash}}?runtime={{runtimeId}}', + blockExplorerAccount: 'https://testnet.oasisscan.com/accounts/detail/{{address}}', }, }, local: { @@ -69,6 +72,7 @@ export const config: BackendConfig = { blockExplorer: 'http://localhost:9001/data/transactions?operation_id={{txHash}}', blockExplorerParatimes: 'http://localhost:9001/data/paratimes/transactions/{{txHash}}?runtime={{runtimeId}}', + blockExplorerAccount: 'http://localhost:9001/data/accounts/detail/{{address}}', }, }, } diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 2f7cf19b84..9df67cc88c 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -40,6 +40,7 @@ "validators": "Validators" }, "summary": { + "activity": "Activity", "balance": { "available": "Available", "debonding": "Debonding", @@ -48,7 +49,10 @@ }, "noTransactionFound": "No transactions found.", "noWalletIsOpen": "To send, receive, stake and swap {{ticker}} tokens, open your wallet.", - "notYourAccount": "This is not your account." + "notYourAccount": "This is not your account.", + "pendingTransactions": "Pending transactions", + "someTxsInPendingState": "Some transactions are currently in a pending state.", + "viewAccountTxs": "View account transactions" }, "transaction": { "addEscrow": { @@ -70,6 +74,7 @@ "header": "Method '{{method}}'" }, "loadingError": "Couldn't load transactions.", + "pending": "Pending", "reclaimEscrow": { "received": " reclaimed by delegator", "sent": "Reclaimed from validator" diff --git a/src/utils/__fixtures__/test-inputs.ts b/src/utils/__fixtures__/test-inputs.ts index 1eca4d07f0..b1e273b0b6 100644 --- a/src/utils/__fixtures__/test-inputs.ts +++ b/src/utils/__fixtures__/test-inputs.ts @@ -49,6 +49,12 @@ export const privateKeyUnlockedState = { transactions: [], transactionsError: undefined, loading: false, + pendingTransactions: { + local: [], + testnet: [], + mainnet: [], + }, + nonce: '0', }, contacts: {}, evmAccounts: {}, @@ -229,6 +235,12 @@ export const walletExtensionV0UnlockedState = { transactions: [], loading: false, allowances: [], + pendingTransactions: { + local: [], + testnet: [], + mainnet: [], + }, + nonce: '0', }, contacts: { oasis1qq3xrq0urs8qcffhvmhfhz4p0mu7ewc8rscnlwxe: { diff --git a/src/vendors/oasisscan.ts b/src/vendors/oasisscan.ts index baf6fef4c1..e0881f3e17 100644 --- a/src/vendors/oasisscan.ts +++ b/src/vendors/oasisscan.ts @@ -206,7 +206,7 @@ export function parseTransactionsList( runtimeName: undefined, runtimeId: undefined, round: undefined, - nonce: BigInt((t as OperationsEntity).nonce ?? 0).toString() + nonce: BigInt((t as OperationsEntity).nonce ?? 0).toString(), } return parsed } From 83819c1e0f6bdad7363ba41ee13bee512bee9360 Mon Sep 17 00:00:00 2001 From: Matej Lubej Date: Wed, 5 Jun 2024 13:06:37 +0200 Subject: [PATCH 03/12] Update test to include pending txs changes --- .../pages/AccountPage/__tests__/index.test.tsx | 5 +++++ .../__tests__/__snapshots__/monitor.test.ts.snap | 14 ++++++++++++++ .../__snapshots__/oasisscan.test.ts.snap | 15 +++++++++++++++ src/vendors/oasisscan.ts | 2 +- 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/app/pages/AccountPage/__tests__/index.test.tsx b/src/app/pages/AccountPage/__tests__/index.test.tsx index f512c55ddd..1e9308a4b1 100644 --- a/src/app/pages/AccountPage/__tests__/index.test.tsx +++ b/src/app/pages/AccountPage/__tests__/index.test.tsx @@ -53,6 +53,11 @@ describe('', () => { transactions: [], accountError: undefined, transactionsError: undefined, + pendingTransactions: { + local: [], + testnet: [], + mainnet: [], + }, }, staking: { delegations: [{ amount: 1111n.toString() }], diff --git a/src/vendors/__tests__/__snapshots__/monitor.test.ts.snap b/src/vendors/__tests__/__snapshots__/monitor.test.ts.snap index ba235c41bf..088015022a 100644 --- a/src/vendors/__tests__/__snapshots__/monitor.test.ts.snap +++ b/src/vendors/__tests__/__snapshots__/monitor.test.ts.snap @@ -6,6 +6,7 @@ exports[`monitor parse account 1`] = ` "available": "7791547645377", "debonding": "0", "delegations": "312240929087242", + "nonce": "89", "total": "320032476732619", } `; @@ -38,6 +39,7 @@ exports[`monitor parse transaction list 1`] = ` "from": "oasis1qz086axf5hreqpehv5hlgmtw7sfem79gz55v68wp", "hash": "b831c4b2aa3188058717250cba279795d907e581bb4d7d40d9dc358d37a56254", "level": 7381105, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -52,6 +54,7 @@ exports[`monitor parse transaction list 1`] = ` "from": "oasis1qz0rx0h3v8fyukfrr0npldrrzvpdg4wj2qxvg0kj", "hash": "e67c4331aa79c85736c4d96cd7b1f3eaad80301bb8d5c181c67482e57ebf0565", "level": 7381138, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -66,6 +69,7 @@ exports[`monitor parse transaction list 1`] = ` "from": "oasis1qzah5wn48ekakmq5405qvcg4czp8hjvrcvcywvhp", "hash": "0558d39e2c5ebe187fc2802ab442faa548013b247b5de1fb1ef75862dad4fb23", "level": 7380979, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -80,6 +84,7 @@ exports[`monitor parse transaction list 1`] = ` "from": "oasis1qzl58e7v7pk50h66s2tv3u9rzf87twp7pcv7hul6", "hash": "ba8e25c66ae31fa0a0837a414359bc2318c6c849515ca3dc1ffa9eb0a1ab92b3", "level": 7361579, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -94,6 +99,7 @@ exports[`monitor parse transaction list 1`] = ` "from": "oasis1qq3833fnmkqe94h0ca6w8qa84sq8pu92qsjmfayj", "hash": "8894b8e9866f66efe291155646f1c09d69d7221449a8d9f758ad1d31f504df03", "level": 7381163, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -108,6 +114,7 @@ exports[`monitor parse transaction list 1`] = ` "from": "oasis1qzr9p9fpjqekr8dev66wuaedcpq5n09hwvpkd4pg", "hash": "d6298496fc19fd95fa1e2b245d1c33661b9ebd7ffb184280c363a31d13210c2a", "level": 7381204, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -122,6 +129,7 @@ exports[`monitor parse transaction list 1`] = ` "from": "oasis1qz6k3gky5d43h70xh2c5vk5fztmzmxmmhc6rh72x", "hash": "46583095fd80becc2683aacc67684170de8d6bc6eca5103d7ac543106d729a8f", "level": 7381052, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -136,6 +144,7 @@ exports[`monitor parse transaction list 1`] = ` "from": "oasis1qpdlqz373hcqvafadd3lxptj42x84sws35s02r4r", "hash": "5378750685efed957417abea41e7d96804264cb51d85dcee45494ef0ca2f31c7", "level": 7368263, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -150,6 +159,7 @@ exports[`monitor parse transaction list 1`] = ` "from": "oasis1qzwl8jlxzwjgz2m6d3ns0vt9hzfp2h63qsxs76ys", "hash": "86a303d9891bbefb0984b82dcc0a51ec190d383248b536dcb8bc9ca0404824f4", "level": 7381231, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -164,6 +174,7 @@ exports[`monitor parse transaction list 1`] = ` "from": "oasis1qrvmxhcjpjvgel9dqfs6zrnza3hqjpa6ug2arc0d", "hash": "2ac0a88ab2c85cef69905c8c9b3f639c5b3b15b969c334df5dcc4fa54f183a8a", "level": 6251849, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -178,6 +189,7 @@ exports[`monitor parse transaction list 1`] = ` "from": "oasis1qp3rhyfjagkj65cnn6lt8ej305gh3kamsvzspluq", "hash": "a62ebc4d30bc045f129f33a14d4019ec88e48c150980ed388d5f64b6e9476059", "level": 4726356, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -192,6 +204,7 @@ exports[`monitor parse transaction list 1`] = ` "from": "oasis1qrd64zucfaugv677fwkhynte4dz450yffgp0k06t", "hash": "f42c704c38ddbab62e83d787e9ff1097c703eac0bcf5587388dd208abae9b888", "level": 7380719, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -206,6 +219,7 @@ exports[`monitor parse transaction list 1`] = ` "from": "oasis1qptn9zmdn5ksvq85vxg2mg84e9m6jp2875dyfl73", "hash": "9c9fd0d2588a0108ec5f277f476483f17e2d8429e947c83f0096b0a7c351aa51", "level": 7381114, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, diff --git a/src/vendors/__tests__/__snapshots__/oasisscan.test.ts.snap b/src/vendors/__tests__/__snapshots__/oasisscan.test.ts.snap index bad7647c66..c11f32c6f3 100644 --- a/src/vendors/__tests__/__snapshots__/oasisscan.test.ts.snap +++ b/src/vendors/__tests__/__snapshots__/oasisscan.test.ts.snap @@ -12,6 +12,7 @@ exports[`oasisscan parse account 1`] = ` "available": "7791547645364", "debonding": "0", "delegations": "312240929087243", + "nonce": "89", "total": "320032476732607", } `; @@ -44,6 +45,7 @@ exports[`oasisscan parse transaction list 1`] = ` "from": "oasis1qqnk4au603zs94k0d0n7c0hkx8t4p6r87s60axru", "hash": "25b84ca4582f6e3140c384ad60b98415d2c3079d1fc5f2221e45da7b13c70817", "level": undefined, + "nonce": undefined, "round": 997775, "runtimeId": "000000000000000000000000000000000000000000000000e2eaa99fc008f87f", "runtimeName": "Emerald", @@ -58,6 +60,7 @@ exports[`oasisscan parse transaction list 1`] = ` "from": "oasis1qz086axf5hreqpehv5hlgmtw7sfem79gz55v68wp", "hash": "b831c4b2aa3188058717250cba279795d907e581bb4d7d40d9dc358d37a56254", "level": 7381105, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -72,6 +75,7 @@ exports[`oasisscan parse transaction list 1`] = ` "from": "oasis1qz0rx0h3v8fyukfrr0npldrrzvpdg4wj2qxvg0kj", "hash": "e67c4331aa79c85736c4d96cd7b1f3eaad80301bb8d5c181c67482e57ebf0565", "level": 7381138, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -86,6 +90,7 @@ exports[`oasisscan parse transaction list 1`] = ` "from": "oasis1qzah5wn48ekakmq5405qvcg4czp8hjvrcvcywvhp", "hash": "0558d39e2c5ebe187fc2802ab442faa548013b247b5de1fb1ef75862dad4fb23", "level": 7380979, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -100,6 +105,7 @@ exports[`oasisscan parse transaction list 1`] = ` "from": "oasis1qzl58e7v7pk50h66s2tv3u9rzf87twp7pcv7hul6", "hash": "ba8e25c66ae31fa0a0837a414359bc2318c6c849515ca3dc1ffa9eb0a1ab92b3", "level": 7361579, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -114,6 +120,7 @@ exports[`oasisscan parse transaction list 1`] = ` "from": "oasis1qq3833fnmkqe94h0ca6w8qa84sq8pu92qsjmfayj", "hash": "8894b8e9866f66efe291155646f1c09d69d7221449a8d9f758ad1d31f504df03", "level": 7381163, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -128,6 +135,7 @@ exports[`oasisscan parse transaction list 1`] = ` "from": "oasis1qzr9p9fpjqekr8dev66wuaedcpq5n09hwvpkd4pg", "hash": "d6298496fc19fd95fa1e2b245d1c33661b9ebd7ffb184280c363a31d13210c2a", "level": 7381204, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -142,6 +150,7 @@ exports[`oasisscan parse transaction list 1`] = ` "from": "oasis1qz6k3gky5d43h70xh2c5vk5fztmzmxmmhc6rh72x", "hash": "46583095fd80becc2683aacc67684170de8d6bc6eca5103d7ac543106d729a8f", "level": 7381052, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -156,6 +165,7 @@ exports[`oasisscan parse transaction list 1`] = ` "from": "oasis1qpdlqz373hcqvafadd3lxptj42x84sws35s02r4r", "hash": "5378750685efed957417abea41e7d96804264cb51d85dcee45494ef0ca2f31c7", "level": 7368263, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -170,6 +180,7 @@ exports[`oasisscan parse transaction list 1`] = ` "from": "oasis1qzwl8jlxzwjgz2m6d3ns0vt9hzfp2h63qsxs76ys", "hash": "86a303d9891bbefb0984b82dcc0a51ec190d383248b536dcb8bc9ca0404824f4", "level": 7381231, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -184,6 +195,7 @@ exports[`oasisscan parse transaction list 1`] = ` "from": "oasis1qrvmxhcjpjvgel9dqfs6zrnza3hqjpa6ug2arc0d", "hash": "2ac0a88ab2c85cef69905c8c9b3f639c5b3b15b969c334df5dcc4fa54f183a8a", "level": 6251849, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -198,6 +210,7 @@ exports[`oasisscan parse transaction list 1`] = ` "from": "oasis1qp3rhyfjagkj65cnn6lt8ej305gh3kamsvzspluq", "hash": "a62ebc4d30bc045f129f33a14d4019ec88e48c150980ed388d5f64b6e9476059", "level": 4726356, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -212,6 +225,7 @@ exports[`oasisscan parse transaction list 1`] = ` "from": "oasis1qrd64zucfaugv677fwkhynte4dz450yffgp0k06t", "hash": "09bfc632625d44fe96d4d31bacd12ed889231f77ed898cbcccf0dea7527a6237", "level": 7396874, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, @@ -226,6 +240,7 @@ exports[`oasisscan parse transaction list 1`] = ` "from": "oasis1qptn9zmdn5ksvq85vxg2mg84e9m6jp2875dyfl73", "hash": "9c9fd0d2588a0108ec5f277f476483f17e2d8429e947c83f0096b0a7c351aa51", "level": 7381114, + "nonce": undefined, "round": undefined, "runtimeId": undefined, "runtimeName": undefined, diff --git a/src/vendors/oasisscan.ts b/src/vendors/oasisscan.ts index e0881f3e17..22addf8e6f 100644 --- a/src/vendors/oasisscan.ts +++ b/src/vendors/oasisscan.ts @@ -206,7 +206,7 @@ export function parseTransactionsList( runtimeName: undefined, runtimeId: undefined, round: undefined, - nonce: BigInt((t as OperationsEntity).nonce ?? 0).toString(), + nonce: (t as OperationsEntity).nonce ? BigInt((t as OperationsEntity).nonce).toString() : undefined, } return parsed } From ef94c2dbc79cf24a49eb17a430ab46556634ee07 Mon Sep 17 00:00:00 2001 From: Matej Lubej Date: Wed, 5 Jun 2024 13:06:46 +0200 Subject: [PATCH 04/12] Add changelog --- .changelog/1954.feature.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changelog/1954.feature.md diff --git a/.changelog/1954.feature.md b/.changelog/1954.feature.md new file mode 100644 index 0000000000..9a26096a2b --- /dev/null +++ b/.changelog/1954.feature.md @@ -0,0 +1,7 @@ +Pending transactions + +Introduces a section for pending transactions within the transaction history +interface. It is designed to display transactions currently in a pending +state that are made within the wallet. The section will also show up in case +there is a discrepancy between transaction history nonce and wallet nonce, +indicating that some transactions are currently in pending state. From d2b734dbe7a6494bd31acaac1a9a8618842765ed Mon Sep 17 00:00:00 2001 From: Matej Lubej Date: Thu, 6 Jun 2024 10:49:36 +0200 Subject: [PATCH 05/12] Remove transactions fetch delay and decrease time to fetch balance --- src/app/state/account/saga.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/state/account/saga.ts b/src/app/state/account/saga.ts index 26f1be73ea..d552e021aa 100644 --- a/src/app/state/account/saga.ts +++ b/src/app/state/account/saga.ts @@ -10,14 +10,13 @@ import { fetchAccount as stakingFetchAccount } from '../staking/saga' import { refreshAccount as walletRefreshAccount } from '../wallet/saga' import { transactionActions } from '../transaction' import { selectAddress } from '../wallet/selectors' -import { selectAccountAddress, selectAccount } from './selectors' +import { selectAccountAddress, selectAccount, hasAccountUnknownPendingTransactions } from './selectors' import { getAccountBalanceWithFallback } from '../../lib/getAccountBalanceWithFallback' import { walletActions } from '../wallet' import { selectSelectedNetwork } from '../network/selectors' import { Transaction } from '../transaction/types' -const ACCOUNT_REFETCHING_INTERVAL = process.env.REACT_APP_E2E_TEST ? 5 * 1000 : 30 * 1000 -const TRANSACTIONS_UPDATE_DELAY = 35 * 1000 // Measured between 8 and 31 second additional delay after balance updates +const ACCOUNT_REFETCHING_INTERVAL = process.env.REACT_APP_E2E_TEST ? 5 * 1000 : 10 * 1000 const TRANSACTIONS_LIMIT = 20 export function* fetchAccount(action: PayloadAction) { @@ -149,13 +148,14 @@ export function* fetchingOnAccountPage() { } const staleBalances = yield* select(selectAccount) + const hasPendingTxs = yield* select(hasAccountUnknownPendingTransactions) if ( staleBalances.available !== refreshedAccount.available || staleBalances.delegations !== refreshedAccount.delegations || - staleBalances.debonding !== refreshedAccount.debonding + staleBalances.debonding !== refreshedAccount.debonding || + hasPendingTxs ) { // Wait for oasisscan to update transactions (it updates balances faster) - yield* delay(TRANSACTIONS_UPDATE_DELAY) yield* call(fetchAccount, startAction) yield* call(stakingFetchAccount, startAction) yield* call(walletRefreshAccount, address) From 1c92a96ec4b87eca2840925fd3c5b732e6a2cd22 Mon Sep 17 00:00:00 2001 From: Matej Lubej Date: Thu, 6 Jun 2024 10:50:24 +0200 Subject: [PATCH 06/12] Show Loading account only on initial load - needs better UX in case it updates, like flash or some animation --- src/app/pages/AccountPage/index.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app/pages/AccountPage/index.tsx b/src/app/pages/AccountPage/index.tsx index fcafa23ed4..e1171099be 100644 --- a/src/app/pages/AccountPage/index.tsx +++ b/src/app/pages/AccountPage/index.tsx @@ -26,9 +26,7 @@ import { walletActions } from 'app/state/wallet' import { AccountSummary } from './Features/AccountSummary' import { AccountPageParams } from './validateAccountPageRoute' -interface AccountPageProps {} - -export function AccountPage(props: AccountPageProps) { +export function AccountPage() { const { t } = useTranslation() const address = useParams().address! const dispatch = useDispatch() @@ -64,10 +62,14 @@ export function AccountPage(props: AccountPageProps) { } }, [dispatch, address, selectedNetwork]) + const isStakeInitialLoading = stake.loading && stake.delegations === null + const isAccountInitialLoading = account.loading && !account.address + return ( {active && } - {(stake.loading || account.loading) && ( + {/* Prevent showing Loading account popup unless initial load */} + {(isStakeInitialLoading || isAccountInitialLoading) && ( From e0f48564d1ffc5d1ffa50e446251f61b6f4dc4bd Mon Sep 17 00:00:00 2001 From: Matej Lubej Date: Mon, 10 Jun 2024 07:26:45 +0200 Subject: [PATCH 07/12] Add unit tests --- .../__snapshots__/index.test.tsx.snap | 3 - .../__tests__/index.test.tsx | 136 +++++++++++++++++- .../Features/TransactionHistory/index.tsx | 32 ++--- 3 files changed, 147 insertions(+), 24 deletions(-) delete mode 100644 src/app/pages/AccountPage/Features/TransactionHistory/__tests__/__snapshots__/index.test.tsx.snap diff --git a/src/app/pages/AccountPage/Features/TransactionHistory/__tests__/__snapshots__/index.test.tsx.snap b/src/app/pages/AccountPage/Features/TransactionHistory/__tests__/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 252c97822b..0000000000 --- a/src/app/pages/AccountPage/Features/TransactionHistory/__tests__/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should match snapshot 1`] = `
`; diff --git a/src/app/pages/AccountPage/Features/TransactionHistory/__tests__/index.test.tsx b/src/app/pages/AccountPage/Features/TransactionHistory/__tests__/index.test.tsx index 5787a519dc..38004f7011 100644 --- a/src/app/pages/AccountPage/Features/TransactionHistory/__tests__/index.test.tsx +++ b/src/app/pages/AccountPage/Features/TransactionHistory/__tests__/index.test.tsx @@ -1,11 +1,139 @@ import * as React from 'react' -import { render } from '@testing-library/react' +import { render, screen } from '@testing-library/react' import { TransactionHistory } from '..' +import { configureAppStore } from '../../../../../../store/configureStore' +import { Provider, useDispatch } from 'react-redux' +import { DeepPartialRootState, RootState } from '../../../../../../types/RootState' +import { Transaction, TransactionStatus, TransactionType } from 'app/state/transaction/types' + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useDispatch: jest.fn(), +})) + +const renderCmp = (store: ReturnType) => + render( + + + , + ) + +const getPendingTx = (hash: string): Transaction => ({ + hash, + type: TransactionType.StakingTransfer, + from: 'oasis1qz0k5q8vjqvu4s4nwxyj406ylnflkc4vrcjghuwk', + amount: 1n.toString(), + to: 'oasis1qz0k5q8vjqvu4s4nwxyj406ylnflkc4vrcjghuww', + status: undefined, + fee: undefined, + level: undefined, + round: undefined, + runtimeId: undefined, + runtimeName: undefined, + timestamp: undefined, + nonce: undefined, +}) + +const getTx = (hash: string, nonce: bigint): Transaction => ({ + ...getPendingTx(hash), + status: TransactionStatus.Successful, + nonce: nonce.toString(), +}) + +const getState = ({ + accountNonce = 0n, + pendingLocalTxs = [], + accountTxs = [], +}: { accountNonce?: bigint; pendingLocalTxs?: Transaction[]; accountTxs?: Transaction[] } = {}) => { + const state: DeepPartialRootState = { + account: { + loading: false, + address: 'oasis1qz0k5q8vjqvu4s4nwxyj406ylnflkc4vrcjghuwk', + available: 100000000000n.toString(), + delegations: null, + debonding: null, + total: null, + transactions: [...accountTxs], + accountError: undefined, + transactionsError: undefined, + pendingTransactions: { + local: [...pendingLocalTxs], + testnet: [], + mainnet: [], + }, + nonce: accountNonce.toString(), + }, + staking: { + delegations: [], + debondingDelegations: [], + }, + wallet: { + selectedWallet: 'oasis1qz0k5q8vjqvu4s4nwxyj406ylnflkc4vrcjghuwk', + wallets: { + oasis1qz0k5q8vjqvu4s4nwxyj406ylnflkc4vrcjghuwk: { + address: 'oasis1qz0k5q8vjqvu4s4nwxyj406ylnflkc4vrcjghuwk', + }, + }, + }, + } + return configureAppStore(state as Partial) +} describe('', () => { - it.skip('should match snapshot', () => { - const component = render() - expect(component.container.firstChild).toMatchSnapshot() + beforeEach(() => { + // Ignore dispatches to fetch account from AccountPage + jest.mocked(useDispatch).mockImplementation(() => jest.fn()) + }) + + it('should not display any pending or completed txs', async () => { + renderCmp(getState()) + + expect(() => screen.getByTestId('pending-txs')).toThrow() + expect(() => screen.getByTestId('completed-txs')).toThrow() + + expect(screen.queryByText('account.summary.someTxsInPendingState')).not.toBeInTheDocument() + expect(await screen.findByText('account.summary.noTransactionFound')).toBeInTheDocument() + }) + + it('should display pending txs alert and no transactions', async () => { + renderCmp(getState({ accountNonce: 1n })) + + expect(() => screen.getByTestId('pending-txs')).toThrow() + expect(() => screen.getByTestId('completed-txs')).toThrow() + + expect(await screen.findByText('account.summary.someTxsInPendingState')).toBeInTheDocument() + expect(await screen.findByText('account.summary.noTransactionFound')).toBeInTheDocument() + expect(await screen.findByRole('link')).toHaveAttribute( + 'href', + 'http://localhost:9001/data/accounts/detail/oasis1qz0k5q8vjqvu4s4nwxyj406ylnflkc4vrcjghuwk', + ) + }) + + it('should display pending txs alert with single pending tx and no completed transactions', async () => { + renderCmp(getState({ accountNonce: 0n, pendingLocalTxs: [getPendingTx('txHash1')] })) + + expect(screen.getByTestId('pending-txs').childElementCount).toBe(1) + expect(() => screen.getByTestId('completed-txs')).toThrow() + + expect(await screen.findByText('account.summary.someTxsInPendingState')).toBeInTheDocument() + expect(await screen.findByText('account.summary.noTransactionFound')).toBeInTheDocument() + expect(await screen.findByText('txHash1')).toBeInTheDocument() + }) + + it('should display single pending and completed tx', async () => { + renderCmp( + getState({ + accountNonce: 2n, + accountTxs: [getTx('txHash1', 0n)], + pendingLocalTxs: [getPendingTx('txHash2')], + }), + ) + + expect(screen.getByTestId('pending-txs').childElementCount).toBe(1) + expect(screen.getByTestId('completed-txs').childElementCount).toBe(1) + + expect(await screen.findByText('txHash1')).toBeInTheDocument() + expect(await screen.findByText('txHash2')).toBeInTheDocument() }) }) diff --git a/src/app/pages/AccountPage/Features/TransactionHistory/index.tsx b/src/app/pages/AccountPage/Features/TransactionHistory/index.tsx index 698179cdc6..bdf624051d 100644 --- a/src/app/pages/AccountPage/Features/TransactionHistory/index.tsx +++ b/src/app/pages/AccountPage/Features/TransactionHistory/index.tsx @@ -46,13 +46,14 @@ export function TransactionHistory() { .map(t => ) return ( - + {transactionsError && (

{t('account.transaction.loadingError', `Couldn't load transactions.`)}{' '}

)} + {/* eslint-disable no-restricted-syntax */} {(!!pendingTransactionComponents.length || hasUnknownPendingTransactions) && ( <> {t('account.summary.pendingTransactions', 'Pending transactions')} @@ -81,27 +82,24 @@ export function TransactionHistory() { } > - - {t( - 'account.summary.someTxsInPendingState', - 'Some transactions are currently in a pending state.', - )} - - - - {pendingTransactionComponents.length ? ( - // eslint-disable-next-line no-restricted-syntax -- pendingTransactionComponents is not a plain text node - pendingTransactionComponents - ) : ( - <> + {t( + 'account.summary.someTxsInPendingState', + 'Some transactions are currently in a pending state.', )} - + + {!!pendingTransactionComponents.length && ( + + {pendingTransactionComponents} + + )} {t('account.summary.activity', 'Activity')} )} + {/* eslint-enable no-restricted-syntax */} {allTransactions.length ? ( - // eslint-disable-next-line no-restricted-syntax -- transactionComponents is not a plain text node - transactionComponents + + {transactionComponents} + ) : ( Date: Wed, 19 Jun 2024 06:30:34 +0200 Subject: [PATCH 08/12] Add E2E test for pending transaction section --- playwright/tests/transfer.spec.ts | 13 +++++++++++++ playwright/utils/mockApi.ts | 18 ++++++++++++++++++ src/app/lib/getAccountBalanceWithFallback.ts | 11 +++++------ src/app/state/account/types.ts | 3 ++- src/app/state/wallet/saga.ts | 12 ++++++++---- src/utils/__fixtures__/test-inputs.ts | 7 ++++--- src/vendors/oasisscan.ts | 8 ++++---- 7 files changed, 54 insertions(+), 18 deletions(-) diff --git a/playwright/tests/transfer.spec.ts b/playwright/tests/transfer.spec.ts index 508cb7829d..188123f5cc 100644 --- a/playwright/tests/transfer.spec.ts +++ b/playwright/tests/transfer.spec.ts @@ -33,3 +33,16 @@ test('Scrolling on amount input field should preserve value', async ({ page }) = await page.mouse.wheel(0, 10) await expect(input).toHaveValue('1111') }) + +test('Should show pending transactions section', async ({ page }) => { + await page.getByTestId('nav-myaccount').click() + + await page.getByPlaceholder('Enter an address').fill('oasis1qrf4y7aelwuusc270e8qx04ysr45w3q0zyavrpdk') + await page.getByPlaceholder('Enter an amount').fill('0.1') + + await page.getByRole('button', { name: /Send/i }).click() + await page.getByRole('button', { name: /Confirm/i }).click() + + await expect(page.getByRole('heading', { name: 'Pending transactions' })).toBeVisible() + await expect(page.getByText('Some transactions are currently in a pending state.')).toBeVisible() +}) diff --git a/playwright/utils/mockApi.ts b/playwright/utils/mockApi.ts index f5a30f5302..9fc67ba396 100644 --- a/playwright/utils/mockApi.ts +++ b/playwright/utils/mockApi.ts @@ -75,6 +75,24 @@ export async function mockApi(context: BrowserContext | Page, balance: number) { body: 'AAAAAAGggAAAAB5ncnBjLXN0YXR1czowDQpncnBjLW1lc3NhZ2U6DQo=', }) }) + await context.route('**/oasis-core.Consensus/GetSignerNonce', route => { + route.fulfill({ + contentType: 'application/grpc-web-text+proto', + body: 'AAAAAAIYKQ==gAAAAB5ncnBjLXN0YXR1czowDQpncnBjLW1lc3NhZ2U6DQo=', + }) + }) + await context.route('**/oasis-core.Consensus/EstimateGas', route => { + route.fulfill({ + contentType: 'application/grpc-web-text+proto', + body: 'AAAAAAMZBPE=gAAAAB5ncnBjLXN0YXR1czowDQpncnBjLW1lc3NhZ2U6DQo=', + }) + }) + await context.route('**/oasis-core.Consensus/SubmitTx', route => { + route.fulfill({ + contentType: 'application/grpc-web-text+proto', + body: 'AAAAAAA=gAAAAB5ncnBjLXN0YXR1czowDQpncnBjLW1lc3NhZ2U6DQo=', + }) + }) // Inside Transak iframe await context.route('https://sentry.io/**', route => route.fulfill({ body: '' })) diff --git a/src/app/lib/getAccountBalanceWithFallback.ts b/src/app/lib/getAccountBalanceWithFallback.ts index 5c73e8e539..c6f70df719 100644 --- a/src/app/lib/getAccountBalanceWithFallback.ts +++ b/src/app/lib/getAccountBalanceWithFallback.ts @@ -3,7 +3,7 @@ import { call } from 'typed-redux-saga' import { getExplorerAPIs, getOasisNic } from '../state/network/saga' import { Account } from '../state/account/types' -function* getBalanceGRPC(address: string) { +function* getBalanceGRPC(address: string, { includeNonce = true } = {}) { const nic = yield* call(getOasisNic) const publicKey = yield* call(addressToPublicKey, address) const account = yield* call([nic, nic.stakingAccount], { owner: publicKey, height: 0 }) @@ -14,20 +14,19 @@ function* getBalanceGRPC(address: string) { delegations: null, debonding: null, total: null, - nonce: account.general?.nonce?.toString() ?? '0', + ...(includeNonce ? { nonce: account.general?.nonce?.toString() ?? '0' } : {}), } } -export function* getAccountBalanceWithFallback(address: string) { +export function* getAccountBalanceWithFallback(address: string, { includeNonce = true } = {}) { const { getAccount } = yield* call(getExplorerAPIs) - try { - const account: Account = yield* call(getAccount, address) + const account: Account = yield* call(getAccount, address, { includeNonce }) return account } catch (apiError: any) { console.error('get account failed, continuing to RPC fallback.', apiError) try { - const account: Account = yield* call(getBalanceGRPC, address) + const account: Account = yield* call(getBalanceGRPC, address, { includeNonce }) return account } catch (rpcError) { console.error('get account with RPC failed, continuing without updated account.', rpcError) diff --git a/src/app/state/account/types.ts b/src/app/state/account/types.ts index 15a2f20ef9..cd3a7b5011 100644 --- a/src/app/state/account/types.ts +++ b/src/app/state/account/types.ts @@ -11,6 +11,7 @@ export interface BalanceDetails { delegations: StringifiedBigInt | null /** This is delayed in getAccount by 20 seconds on oasisscan and 5 seconds on oasismonitor. */ total: StringifiedBigInt | null + nonce?: StringifiedBigInt | null } export interface Allowance { @@ -21,7 +22,7 @@ export interface Allowance { export interface Account extends BalanceDetails { address: string allowances?: Allowance[] - nonce: StringifiedBigInt + nonce?: StringifiedBigInt } /* --- STATE --- */ diff --git a/src/app/state/wallet/saga.ts b/src/app/state/wallet/saga.ts index 09c95f1b41..76f7a0afa2 100644 --- a/src/app/state/wallet/saga.ts +++ b/src/app/state/wallet/saga.ts @@ -48,11 +48,13 @@ export function* openWalletsFromLedger({ payload }: PayloadAction) { const wallet = action.payload - const balance = yield* call(getAccountBalanceWithFallback, wallet.address) + const balance = yield* call(getAccountBalanceWithFallback, wallet.address, { includeNonce: false }) yield* put( walletActions.updateBalance({ address: wallet.address, diff --git a/src/utils/__fixtures__/test-inputs.ts b/src/utils/__fixtures__/test-inputs.ts index b1e273b0b6..260f991fbb 100644 --- a/src/utils/__fixtures__/test-inputs.ts +++ b/src/utils/__fixtures__/test-inputs.ts @@ -54,7 +54,7 @@ export const privateKeyUnlockedState = { testnet: [], mainnet: [], }, - nonce: '0', + nonce: '1', }, contacts: {}, evmAccounts: {}, @@ -121,6 +121,7 @@ export const privateKeyUnlockedState = { available: '0', debonding: '0', delegations: '0', + nonce: '1', total: '0', validator: { escrow: '0', @@ -240,7 +241,7 @@ export const walletExtensionV0UnlockedState = { testnet: [], mainnet: [], }, - nonce: '0', + nonce: '1', }, contacts: { oasis1qq3xrq0urs8qcffhvmhfhz4p0mu7ewc8rscnlwxe: { @@ -289,7 +290,6 @@ export const walletExtensionV0UnlockedState = { ethPrivateKeyRaw: '', feeAmount: '', feeGas: '', - paraTime: undefined, recipient: '', type: undefined, }, @@ -385,6 +385,7 @@ export const walletExtensionV0UnlockedState = { debonding: '0', delegations: '0', total: '0', + nonce: '0', }, name: 'short privatekey', privateKey: diff --git a/src/vendors/oasisscan.ts b/src/vendors/oasisscan.ts index 22addf8e6f..fc473a2a51 100644 --- a/src/vendors/oasisscan.ts +++ b/src/vendors/oasisscan.ts @@ -33,10 +33,10 @@ export function getOasisscanAPIs(url: string | 'https://api.oasisscan.com/mainne const operationsEntity = new OperationsEntityApi(explorerConfig) const runtime = new RuntimeApi(explorerConfig) - async function getAccount(address: string): Promise { + async function getAccount(address: string, { includeNonce = true } = {}): Promise { const account = await accounts.getAccount({ accountId: address }) if (!account || account.code !== 0) throw new Error('Wrong response code') // TODO - return parseAccount(account.data) + return parseAccount(account.data, { includeNonce }) } async function getAllValidators(): Promise { @@ -103,7 +103,7 @@ export function getOasisscanAPIs(url: string | 'https://api.oasisscan.com/mainne } } -export function parseAccount(account: AccountsRow): Account { +export function parseAccount(account: AccountsRow, { includeNonce = true } = {}): Account { return { address: account.address, allowances: account.allowances.map(allowance => ({ @@ -114,7 +114,7 @@ export function parseAccount(account: AccountsRow): Account { delegations: parseRoseStringToBaseUnitString(account.escrow), debonding: parseRoseStringToBaseUnitString(account.debonding), total: parseRoseStringToBaseUnitString(account.total), - nonce: BigInt(account.nonce ?? 0).toString(), + ...(includeNonce ? { nonce: BigInt(account.nonce ?? 0).toString() } : {}), } } From d344b48918ca6d2cceda3b5b68964125662f3a40 Mon Sep 17 00:00:00 2001 From: Matej Lubej Date: Thu, 20 Jun 2024 07:58:21 +0200 Subject: [PATCH 09/12] Merge only nonce from tx detail request --- src/app/state/account/saga.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/state/account/saga.ts b/src/app/state/account/saga.ts index d552e021aa..811f5a9fe6 100644 --- a/src/app/state/account/saga.ts +++ b/src/app/state/account/saga.ts @@ -59,13 +59,13 @@ export function* fetchAccount(action: PayloadAction) { const detailedTransactions = yield* call(() => Promise.allSettled(transactions.map(({ hash }) => getTransaction({ hash }))), ) - const transactionsWithDetails = transactions.map((t, i) => { + const transactionsWithUpdatedNonce = transactions.map((t, i) => { const { status, value } = detailedTransactions[i] as PromiseFulfilledResult - // Skip failed txs + // Skip in case transaction detail request failed if (status === 'fulfilled') { return { ...t, - ...value, + nonce: value.nonce, } } @@ -73,7 +73,7 @@ export function* fetchAccount(action: PayloadAction) { }) yield* put( - accountActions.transactionsLoaded({ networkType, transactions: transactionsWithDetails }), + accountActions.transactionsLoaded({ networkType, transactions: transactionsWithUpdatedNonce }), ) } catch (e: any) { console.error('get transactions list failed, continuing without updated list.', e) From 2ee55c9b9c7d5953b0b36505663c17df291d0ccd Mon Sep 17 00:00:00 2001 From: Matej Lubej Date: Thu, 27 Jun 2024 07:47:02 +0200 Subject: [PATCH 10/12] Fix transaction detail nonce parsing --- .../__tests__/index.test.tsx | 38 +++++++++++++++++-- src/app/state/account/selectors.ts | 2 +- src/vendors/oasisscan.ts | 3 +- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/app/pages/AccountPage/Features/TransactionHistory/__tests__/index.test.tsx b/src/app/pages/AccountPage/Features/TransactionHistory/__tests__/index.test.tsx index 38004f7011..65523a5ffc 100644 --- a/src/app/pages/AccountPage/Features/TransactionHistory/__tests__/index.test.tsx +++ b/src/app/pages/AccountPage/Features/TransactionHistory/__tests__/index.test.tsx @@ -35,9 +35,9 @@ const getPendingTx = (hash: string): Transaction => ({ nonce: undefined, }) -const getTx = (hash: string, nonce: bigint): Transaction => ({ +const getTx = ({ hash = '', nonce = 0n, status = TransactionStatus.Successful } = {}): Transaction => ({ ...getPendingTx(hash), - status: TransactionStatus.Successful, + status, nonce: nonce.toString(), }) @@ -125,7 +125,7 @@ describe('', () => { renderCmp( getState({ accountNonce: 2n, - accountTxs: [getTx('txHash1', 0n)], + accountTxs: [getTx({ hash: 'txHash1', nonce: 0n })], pendingLocalTxs: [getPendingTx('txHash2')], }), ) @@ -136,4 +136,36 @@ describe('', () => { expect(await screen.findByText('txHash1')).toBeInTheDocument() expect(await screen.findByText('txHash2')).toBeInTheDocument() }) + + it('should not display pending section in case of failed tx', async () => { + renderCmp( + getState({ + accountNonce: 1n, + accountTxs: [getTx({ hash: 'txHash1', nonce: 0n, status: TransactionStatus.Failed })], + }), + ) + + expect(() => screen.getByTestId('pending-txs')).toThrow() + expect(screen.getByTestId('completed-txs').childElementCount).toBe(1) + + expect(await screen.findByText('txHash1')).toBeInTheDocument() + + expect(() => screen.getByText('account.summary.someTxsInPendingState')).toThrow() + }) + + it('should not display pending section on initial load', async () => { + renderCmp( + getState({ + accountNonce: 1n, + accountTxs: [getTx({ hash: 'txHash1', nonce: 0n, status: TransactionStatus.Successful })], + }), + ) + + expect(() => screen.getByTestId('pending-txs')).toThrow() + expect(screen.getByTestId('completed-txs').childElementCount).toBe(1) + + expect(await screen.findByText('txHash1')).toBeInTheDocument() + + expect(() => screen.getByText('account.summary.someTxsInPendingState')).toThrow() + }) }) diff --git a/src/app/state/account/selectors.ts b/src/app/state/account/selectors.ts index 9bbde4e7c4..a112c88549 100644 --- a/src/app/state/account/selectors.ts +++ b/src/app/state/account/selectors.ts @@ -47,7 +47,7 @@ export const hasAccountUnknownPendingTransactions = createSelector( undefined as bigint | undefined, ) - if (!maxNonceFromTxs) { + if (maxNonceFromTxs === undefined) { return BigInt(accountNonce) > 0n } diff --git a/src/vendors/oasisscan.ts b/src/vendors/oasisscan.ts index fc473a2a51..51614d630d 100644 --- a/src/vendors/oasisscan.ts +++ b/src/vendors/oasisscan.ts @@ -206,7 +206,8 @@ export function parseTransactionsList( runtimeName: undefined, runtimeId: undefined, round: undefined, - nonce: (t as OperationsEntity).nonce ? BigInt((t as OperationsEntity).nonce).toString() : undefined, + nonce: + (t as OperationsEntity).nonce >= 0 ? BigInt((t as OperationsEntity).nonce).toString() : undefined, } return parsed } From 5e8f2fd35070ea5dc9d94c1456bfc8e44b332893 Mon Sep 17 00:00:00 2001 From: Matej Lubej Date: Thu, 27 Jun 2024 09:02:46 +0200 Subject: [PATCH 11/12] Fix pending section showing up on initial account load --- .../pages/AccountPage/Features/TransactionHistory/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/pages/AccountPage/Features/TransactionHistory/index.tsx b/src/app/pages/AccountPage/Features/TransactionHistory/index.tsx index bdf624051d..6a33c09217 100644 --- a/src/app/pages/AccountPage/Features/TransactionHistory/index.tsx +++ b/src/app/pages/AccountPage/Features/TransactionHistory/index.tsx @@ -13,6 +13,7 @@ import { useSelector } from 'react-redux' import { hasAccountUnknownPendingTransactions, selectAccountAddress, + selectAccountIsLoading, selectPendingTransactionForAccount, selectTransactions, selectTransactionsError, @@ -34,6 +35,7 @@ export function TransactionHistory() { const allTransactions = useSelector(selectTransactions) const transactionsError = useSelector(selectTransactionsError) const address = useSelector(selectAccountAddress) + const accountIsLoading = useSelector(selectAccountIsLoading) const pendingTransactions = useSelector(selectPendingTransactionForAccount) const hasUnknownPendingTransactions = useSelector(hasAccountUnknownPendingTransactions) const network = useSelector(selectSelectedNetwork) @@ -45,6 +47,8 @@ export function TransactionHistory() { .filter(({ hash: pendingTxHash }) => !allTransactions.some(({ hash }) => hash === pendingTxHash)) .map(t => ) + const showPendingSection = !accountIsLoading && !!address + return ( {transactionsError && ( @@ -54,7 +58,7 @@ export function TransactionHistory() {

)} {/* eslint-disable no-restricted-syntax */} - {(!!pendingTransactionComponents.length || hasUnknownPendingTransactions) && ( + {showPendingSection && (!!pendingTransactionComponents.length || hasUnknownPendingTransactions) && ( <> {t('account.summary.pendingTransactions', 'Pending transactions')} Date: Thu, 4 Jul 2024 09:10:46 +0200 Subject: [PATCH 12/12] Unify Account and WalletBalance interface to always include nonce --- .../Features/Account/__tests__/Account.test.tsx | 2 +- .../AccountSelector/__tests__/index.test.tsx | 2 +- src/app/lib/getAccountBalanceWithFallback.ts | 10 +++++----- src/app/pages/AccountPage/index.tsx | 1 + .../__tests__/index.test.tsx | 6 +++--- src/app/state/account/types.ts | 3 +-- src/app/state/wallet/saga.ts | 8 ++------ src/utils/__fixtures__/test-inputs.ts | 6 ++++++ .../__snapshots__/walletExtensionV0.test.ts.snap | 7 +++++++ src/utils/walletExtensionV0.ts | 3 +++ src/vendors/oasisscan.ts | 12 +++++++----- 11 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/app/components/Toolbar/Features/Account/__tests__/Account.test.tsx b/src/app/components/Toolbar/Features/Account/__tests__/Account.test.tsx index d7d2e955aa..5af4e9ee45 100644 --- a/src/app/components/Toolbar/Features/Account/__tests__/Account.test.tsx +++ b/src/app/components/Toolbar/Features/Account/__tests__/Account.test.tsx @@ -7,7 +7,7 @@ import { Account, AccountProps } from '../Account' const props = { address: 'oasis1qq3xrq0urs8qcffhvmhfhz4p0mu7ewc8rscnlwxe', - balance: { available: '200', debonding: '0', delegations: '800', total: '1000' }, + balance: { available: '200', debonding: '0', delegations: '800', total: '1000', nonce: '0' }, onClick: () => {}, isActive: false, displayBalance: true, diff --git a/src/app/components/Toolbar/Features/AccountSelector/__tests__/index.test.tsx b/src/app/components/Toolbar/Features/AccountSelector/__tests__/index.test.tsx index 165eaf5667..f0830caf73 100644 --- a/src/app/components/Toolbar/Features/AccountSelector/__tests__/index.test.tsx +++ b/src/app/components/Toolbar/Features/AccountSelector/__tests__/index.test.tsx @@ -28,7 +28,7 @@ describe('', () => { wallets: { oasis1qq3xrq0urs8qcffhvmhfhz4p0mu7ewc8rscnlwxe: { address: 'oasis1qq3xrq0urs8qcffhvmhfhz4p0mu7ewc8rscnlwxe', - balance: { available: '100', debonding: '0', delegations: '0', total: '100' }, + balance: { available: '100', debonding: '0', delegations: '0', total: '100', nonce: '0' }, publicKey: '00', type: WalletType.UsbLedger, }, diff --git a/src/app/lib/getAccountBalanceWithFallback.ts b/src/app/lib/getAccountBalanceWithFallback.ts index c6f70df719..a9144e308e 100644 --- a/src/app/lib/getAccountBalanceWithFallback.ts +++ b/src/app/lib/getAccountBalanceWithFallback.ts @@ -3,7 +3,7 @@ import { call } from 'typed-redux-saga' import { getExplorerAPIs, getOasisNic } from '../state/network/saga' import { Account } from '../state/account/types' -function* getBalanceGRPC(address: string, { includeNonce = true } = {}) { +function* getBalanceGRPC(address: string) { const nic = yield* call(getOasisNic) const publicKey = yield* call(addressToPublicKey, address) const account = yield* call([nic, nic.stakingAccount], { owner: publicKey, height: 0 }) @@ -14,19 +14,19 @@ function* getBalanceGRPC(address: string, { includeNonce = true } = {}) { delegations: null, debonding: null, total: null, - ...(includeNonce ? { nonce: account.general?.nonce?.toString() ?? '0' } : {}), + nonce: account.general?.nonce?.toString() ?? '0', } } -export function* getAccountBalanceWithFallback(address: string, { includeNonce = true } = {}) { +export function* getAccountBalanceWithFallback(address: string) { const { getAccount } = yield* call(getExplorerAPIs) try { - const account: Account = yield* call(getAccount, address, { includeNonce }) + const account: Account = yield* call(getAccount, address) return account } catch (apiError: any) { console.error('get account failed, continuing to RPC fallback.', apiError) try { - const account: Account = yield* call(getBalanceGRPC, address, { includeNonce }) + const account: Account = yield* call(getBalanceGRPC, address) return account } catch (rpcError) { console.error('get account with RPC failed, continuing without updated account.', rpcError) diff --git a/src/app/pages/AccountPage/index.tsx b/src/app/pages/AccountPage/index.tsx index e1171099be..e0207ace2c 100644 --- a/src/app/pages/AccountPage/index.tsx +++ b/src/app/pages/AccountPage/index.tsx @@ -52,6 +52,7 @@ export function AccountPage() { account.available == null || balanceDelegations == null || balanceDebondingDelegations == null ? null : (BigInt(account.available) + balanceDelegations + balanceDebondingDelegations).toString(), + nonce: account.nonce, } // Restart fetching account balances if address or network changes diff --git a/src/app/pages/OpenWalletPage/Features/ImportAccountsSelectionModal/__tests__/index.test.tsx b/src/app/pages/OpenWalletPage/Features/ImportAccountsSelectionModal/__tests__/index.test.tsx index 10269e5291..44cb2ae264 100644 --- a/src/app/pages/OpenWalletPage/Features/ImportAccountsSelectionModal/__tests__/index.test.tsx +++ b/src/app/pages/OpenWalletPage/Features/ImportAccountsSelectionModal/__tests__/index.test.tsx @@ -52,7 +52,7 @@ describe('', () => { importAccountsActions.accountsListed([ { address: 'oasis1qzyqaxestzlum26e2vdgvkerm6d9qgdp7gh2pxqe', - balance: { available: '0', debonding: '0', delegations: '0', total: '0' }, + balance: { available: '0', debonding: '0', delegations: '0', total: '0', nonce: '0' }, path: [44, 474, 0], pathDisplay: `m/44'/474'/0'`, publicKey: '00', @@ -77,7 +77,7 @@ describe('', () => { importAccountsActions.accountsListed([ { address: 'oasis1qzyqaxestzlum26e2vdgvkerm6d9qgdp7gh2pxqe', - balance: { available: '0', debonding: '0', delegations: '0', total: '0' }, + balance: { available: '0', debonding: '0', delegations: '0', total: '0', nonce: '0' }, path: [44, 474, 0], pathDisplay: `m/44'/474'/0'`, publicKey: '00', @@ -86,7 +86,7 @@ describe('', () => { }, { address: 'oasis1qqv25adrld8jjquzxzg769689lgf9jxvwgjs8tha', - balance: { available: '0', debonding: '0', delegations: '0', total: '0' }, + balance: { available: '0', debonding: '0', delegations: '0', total: '0', nonce: '0' }, path: [44, 474, 1], pathDisplay: `m/44'/474'/1'`, publicKey: '00', diff --git a/src/app/state/account/types.ts b/src/app/state/account/types.ts index cd3a7b5011..46f0554fbc 100644 --- a/src/app/state/account/types.ts +++ b/src/app/state/account/types.ts @@ -11,7 +11,7 @@ export interface BalanceDetails { delegations: StringifiedBigInt | null /** This is delayed in getAccount by 20 seconds on oasisscan and 5 seconds on oasismonitor. */ total: StringifiedBigInt | null - nonce?: StringifiedBigInt | null + nonce: StringifiedBigInt } export interface Allowance { @@ -22,7 +22,6 @@ export interface Allowance { export interface Account extends BalanceDetails { address: string allowances?: Allowance[] - nonce?: StringifiedBigInt } /* --- STATE --- */ diff --git a/src/app/state/wallet/saga.ts b/src/app/state/wallet/saga.ts index 76f7a0afa2..7f82b19c69 100644 --- a/src/app/state/wallet/saga.ts +++ b/src/app/state/wallet/saga.ts @@ -48,13 +48,11 @@ export function* openWalletsFromLedger({ payload }: PayloadAction { + async function getAccount(address: string): Promise { const account = await accounts.getAccount({ accountId: address }) if (!account || account.code !== 0) throw new Error('Wrong response code') // TODO - return parseAccount(account.data, { includeNonce }) + return parseAccount(account.data) } async function getAllValidators(): Promise { @@ -103,7 +103,7 @@ export function getOasisscanAPIs(url: string | 'https://api.oasisscan.com/mainne } } -export function parseAccount(account: AccountsRow, { includeNonce = true } = {}): Account { +export function parseAccount(account: AccountsRow): Account { return { address: account.address, allowances: account.allowances.map(allowance => ({ @@ -114,7 +114,7 @@ export function parseAccount(account: AccountsRow, { includeNonce = true } = {}) delegations: parseRoseStringToBaseUnitString(account.escrow), debonding: parseRoseStringToBaseUnitString(account.debonding), total: parseRoseStringToBaseUnitString(account.total), - ...(includeNonce ? { nonce: BigInt(account.nonce ?? 0).toString() } : {}), + nonce: BigInt(account.nonce ?? 0).toString(), } } @@ -207,7 +207,9 @@ export function parseTransactionsList( runtimeId: undefined, round: undefined, nonce: - (t as OperationsEntity).nonce >= 0 ? BigInt((t as OperationsEntity).nonce).toString() : undefined, + (t as OperationsEntity).nonce == null + ? undefined + : BigInt((t as OperationsEntity).nonce).toString(), } return parsed }