diff --git a/packages/dag4-network/src/api/v2/block-explorer-api.ts b/packages/dag4-network/src/api/v2/block-explorer-api.ts index a6c515c..c8814d6 100644 --- a/packages/dag4-network/src/api/v2/block-explorer-api.ts +++ b/packages/dag4-network/src/api/v2/block-explorer-api.ts @@ -1,12 +1,16 @@ -import {RestApi} from '@stardust-collective/dag4-core'; -import {DNC} from '../../DNC'; +import { RestApi } from '@stardust-collective/dag4-core'; +import { DNC } from '../../DNC'; import { - SnapshotV2, - TransactionV2, - RewardTransaction, - AddressBalanceV2, + ActionType, + ActionResponse, + AddressBalanceV2, + AllowSpendResponse, BlockV2, - CurrencySnapshotV2 + CurrencySnapshotV2, + RewardTransaction, + SnapshotV2, + TokenLockResponse, + TransactionV2 } from '../../dto/v2'; type HashOrOrdinal = string | number; @@ -70,22 +74,34 @@ export class BlockExplorerV2Api { limit = null, searchAfter = null, searchBefore = null, - next = null + next = null, + actionType = null, + active = false } : { limit?: number, searchAfter?: string, searchBefore?: string, - next?: string + next?: string, + actionType?: ActionType, + active?: boolean }) { let params; - if (limit || searchAfter || searchBefore || next) { + if (limit || searchAfter || searchBefore || next || active) { params = {}; if (limit && limit > 0) { params.limit = limit; } + if (actionType) { + params.transactionType = actionType; + } + + if (active) { + params.active = active; + } + // search_after, search_before and next are mutually exclusive if (searchAfter) { params.search_after = searchAfter; @@ -173,11 +189,47 @@ export class BlockExplorerV2Api { return this.service.$get>(`/currency/${metagraphId}/addresses/${address}/transactions${searchPath}`, params); } + async getCurrencyActionsByAddress(metagraphId: string, address: string, actionType?: ActionType, limit?: number, searchAfter?: string, searchBefore?: string, next?: string) { + const params = this.buildRequestParams({ limit, searchAfter, searchBefore, next, actionType }); + + return this.service.$get>(`/currency/${metagraphId}/addresses/${address}/actions`, params); + } + async getCurrencyTransactionsBySnapshot(metagraphId: string, hashOrOrdinal: string, limit?: number, searchAfter?: string, searchBefore?: string, next?: string) { const params = this.buildRequestParams({ limit, searchAfter, searchBefore, next }); return this.service.$get>(`/currency/${metagraphId}/snapshots/${hashOrOrdinal}/transactions`, params); } + + async getCurrencyTokenLocksByAddress(metagraphId: string, address: string, limit?: number, searchAfter?: string, searchBefore?: string, next?: string, active?: boolean) { + const params = this.buildRequestParams({ limit, searchAfter, searchBefore, next, active }); + + return this.service.$get>(`/currency/${metagraphId}/addresses/${address}/token-locks`, params); + } + + async getCurrencyAllowSpendsByAddress(metagraphId: string, address: string, limit?: number, searchAfter?: string, searchBefore?: string, next?: string, active?: boolean) { + const params = this.buildRequestParams({ limit, searchAfter, searchBefore, next, active }); + + return this.service.$get>(`/currency/${metagraphId}/addresses/${address}/allow-spends`, params); + } + + async getActionsByAddress(address: string, actionType?: ActionType, limit?: number, searchAfter?: string, searchBefore?: string, next?: string) { + const params = this.buildRequestParams({ limit, searchAfter, searchBefore, next, actionType }); + + return this.service.$get>(`/addresses/${address}/actions`, params); + } + + async getTokenLocksByAddress(address: string, limit?: number, searchAfter?: string, searchBefore?: string, next?: string, active?: boolean) { + const params = this.buildRequestParams({ limit, searchAfter, searchBefore, next, active }); + + return this.service.$get>(`/addresses/${address}/token-locks`, params); + } + + async getAllowSpendsByAddress(address: string, limit?: number, searchAfter?: string, searchBefore?: string, next?: string, active?: boolean) { + const params = this.buildRequestParams({ limit, searchAfter, searchBefore, next, active }); + + return this.service.$get>(`/addresses/${address}/allow-spends`, params); + } } export const blockExplorerApi = new BlockExplorerV2Api(); diff --git a/packages/dag4-network/src/dag-network.ts b/packages/dag4-network/src/dag-network.ts index e51e172..5671429 100644 --- a/packages/dag4-network/src/dag-network.ts +++ b/packages/dag4-network/src/dag-network.ts @@ -1,12 +1,12 @@ -import {Subject} from 'rxjs'; -import {NetworkInfo} from './types/network-info'; -import {LoadBalancerApi} from './api/v1/load-balancer-api'; -import {BlockExplorerApi} from './api/v1/block-explorer-api'; -import {L0Api} from './api/v2/l0-api'; -import {L1Api} from './api/v2/l1-api'; -import {BlockExplorerV2Api} from './api/v2/block-explorer-api'; -import {PostTransactionV2, PendingTransaction, TransactionV2, SnapshotV2} from './dto/v2'; -import {PostTransaction, CbTransaction, Transaction, Snapshot} from './dto/v1'; +import { Subject } from 'rxjs'; +import { BlockExplorerApi } from './api/v1/block-explorer-api'; +import { LoadBalancerApi } from './api/v1/load-balancer-api'; +import { BlockExplorerV2Api } from './api/v2/block-explorer-api'; +import { L0Api } from './api/v2/l0-api'; +import { L1Api } from './api/v2/l1-api'; +import { CbTransaction, PostTransaction, Snapshot, Transaction } from './dto/v1'; +import { PendingTransaction, PostTransactionV2, SnapshotV2, TransactionV2 } from './dto/v2'; +import { NetworkInfo } from './types/network-info'; export class DagNetwork { private connectedNetwork: NetworkInfo = { diff --git a/packages/dag4-network/src/dto/v2/index.ts b/packages/dag4-network/src/dto/v2/index.ts index 47557a5..a3d20da 100644 --- a/packages/dag4-network/src/dto/v2/index.ts +++ b/packages/dag4-network/src/dto/v2/index.ts @@ -1,9 +1,11 @@ -export * from './snapshot'; -export * from './transaction'; -export * from './reward-transaction'; export * from './address-balance'; export * from './block'; +export * from './metagraph'; export * from './peer'; -export * from './total-supply'; +export * from './reward-transaction'; +export * from './snapshot'; +export * from './staking'; export * from './swap-operations'; -export * from './metagraph'; +export * from './total-supply'; +export * from './transaction'; + diff --git a/packages/dag4-network/src/dto/v2/staking.ts b/packages/dag4-network/src/dto/v2/staking.ts new file mode 100644 index 0000000..81ebc0d --- /dev/null +++ b/packages/dag4-network/src/dto/v2/staking.ts @@ -0,0 +1,47 @@ +export type TokenLockResponse = { + currencyId: string | null; + hash: string; + ordinal: number; + amount: number; + source: string; + timestamp: string; + globalSnapshotHash: string; + globalSnapshotOrdinal: number; + + unlockEpoch: number | null; + parentHash: string; + unlockedAtOrdinal: number | null; +}; + +export type AllowSpendResponse = { + currencyId: string | null; + hash: string; + ordinal: number; + amount: number; + source: string; + timestamp: string; + globalSnapshotHash: string; + globalSnapshotOrdinal: number; + + destination: string; + lastValidEpochProgress: number; + fee: number; + snapshotHash: string; +} + +export const Actions = ["TokenLock", "TokenUnlock", "AllowSpend", "ExpiredAllowSpend", "DelegateStakeCreate", "DelegateStakeWithdraw", "SpendTransaction", "FeeTransaction", "ExpiredSpendTransaction"] as const; +export type ActionType = (typeof Actions)[number]; + +export type ActionResponse = { + type: ActionType; + currencyId: string | null; + hash: string; + amount: number; + source: string; + destination: string | null; + unlockEpoch: number | null; + parentHash: string; + timestamp: string; + globalSnapshotHash: string; + globalSnapshotOrdinal: number; +} \ No newline at end of file diff --git a/packages/dag4-network/src/dto/v2/transaction.ts b/packages/dag4-network/src/dto/v2/transaction.ts index 00ee48d..dc7af34 100644 --- a/packages/dag4-network/src/dto/v2/transaction.ts +++ b/packages/dag4-network/src/dto/v2/transaction.ts @@ -1,4 +1,5 @@ import { BigNumber } from "bignumber.js"; +import type { Transaction } from "../v1"; export type TransactionReference = { hash: string diff --git a/packages/dag4-network/src/metagraph-token-network.ts b/packages/dag4-network/src/metagraph-token-network.ts index 1506b3a..30d2ac7 100644 --- a/packages/dag4-network/src/metagraph-token-network.ts +++ b/packages/dag4-network/src/metagraph-token-network.ts @@ -1,14 +1,18 @@ -import { MetagraphNetworkInfo } from "./types/network-info"; +import { MetagraphTokenDataL1Api } from "./api/metagraph-token/data-l1-api"; +import { MetagraphTokenL0Api } from "./api/metagraph-token/l0-api"; +import { MetagraphTokenL1Api } from "./api/metagraph-token/l1-api"; +import { BlockExplorerV2Api } from './api/v2/block-explorer-api'; import { - PostTransactionV2, + ActionType, + ActionResponse, + CurrencySnapshotV2, PendingTransaction, + PostTransactionV2, TransactionV2, - CurrencySnapshotV2 + AllowSpendResponse, + TokenLockResponse } from "./dto/v2"; -import {BlockExplorerV2Api} from './api/v2/block-explorer-api'; -import { MetagraphTokenL0Api } from "./api/metagraph-token/l0-api"; -import { MetagraphTokenL1Api } from "./api/metagraph-token/l1-api"; -import { MetagraphTokenDataL1Api } from "./api/metagraph-token/data-l1-api"; +import { MetagraphNetworkInfo } from "./types/network-info"; class MetagraphTokenNetwork { private connectedNetwork: MetagraphNetworkInfo; @@ -69,6 +73,71 @@ class MetagraphTokenNetwork { return response ? response.data : null; } + async getActionsByAddress( + address: string, + actionType?: ActionType, + limit?: number, + searchAfter?: string, + searchBefore?: string, + next?: string + ): Promise { + const actions = await this.beApi.getCurrencyActionsByAddress(this.connectedNetwork.metagraphId, address, actionType, limit, searchAfter, searchBefore, next); + + if (!actions?.data?.length) return []; + + return actions.data; + } + + async getActiveTokenLocksTransactions(address: string, limit?: number, searchAfter?: string, searchBefore?: string): Promise { + const activeTokenLocks: TokenLockResponse[] = []; + let next: string | undefined; + + do { + const response = await this.beApi.getCurrencyTokenLocksByAddress( + this.connectedNetwork.metagraphId, + address, + limit, + searchAfter, + searchBefore, + next, + true, + ); + + if (response?.data) { + activeTokenLocks.push(...response.data); + } + + next = response?.meta?.next; + } while (next); + + return activeTokenLocks; + }; + + async getActiveAllowSpendsTransactions(address: string, limit?: number, searchAfter?: string, searchBefore?: string): Promise { + const activeAllowSpends: AllowSpendResponse[] = []; + let next: string | undefined; + + do { + const response = await this.beApi.getCurrencyAllowSpendsByAddress( + this.connectedNetwork.metagraphId, + address, + limit, + searchAfter, + searchBefore, + next, + true, + ); + + if (response?.data) { + activeAllowSpends.push(...response.data); + } + + next = response?.meta?.next; + } while (next); + + return activeAllowSpends; + }; + async getTransaction( hash: string | null ): Promise { @@ -121,4 +190,4 @@ class MetagraphTokenNetwork { } } -export { MetagraphTokenNetwork }; \ No newline at end of file +export { MetagraphTokenNetwork }; diff --git a/packages/dag4-wallet/src/dag-account.ts b/packages/dag4-wallet/src/dag-account.ts index e11acf7..f8d6768 100644 --- a/packages/dag4-wallet/src/dag-account.ts +++ b/packages/dag4-wallet/src/dag-account.ts @@ -1,44 +1,48 @@ +import { DAG_DECIMALS } from "@stardust-collective/dag4-core"; import { keyStore, KeyTrio, PostTransaction, PostTransactionV2, } from "@stardust-collective/dag4-keystore"; -import { DAG_DECIMALS } from "@stardust-collective/dag4-core"; import { + ActionType, + ActionResponse, + AllowSpend, + AllowSpendResponse, + AllowSpendWithCurrencyId, + DagNetwork, + DelegatedStake, + DelegatedStakeWithParent, globalDagNetwork, GlobalDagNetwork, - DagNetwork, + HashResponse, + MetagraphNetworkInfo, NetworkInfo, PendingTx, - TransactionReference, - MetagraphNetworkInfo, - AllowSpendWithCurrencyId, - AllowSpend, - TokenLock, - TokenLockWithCurrencyId, - DelegatedStake, - WithdrawDelegatedStake, - HashResponse, SignedDelegatedStake, SignedWithdrawDelegatedStake, - DelegatedStakeWithParent, + TokenLock, + TokenLockResponse, + TokenLockWithCurrencyId, + TransactionReference, + WithdrawDelegatedStake } from "@stardust-collective/dag4-network"; import { BigNumber } from "bignumber.js"; import { Subject } from "rxjs"; -import { networkConfig } from "./network-config"; import { MetagraphTokenClient } from "./metagraph-token-client"; +import { networkConfig } from "./network-config"; +import { allowSpend, tokenLock } from "./shared/operations"; import { normalizePublicKey } from "./utils"; import { - validateSchema, + dagAddressValidator, delegatedStakeSchema, - withdrawDelegatedStakeSchema, postAllowSpendSchema, - validateArraySchema, - dagAddressValidator, postTokenLockSchema, + validateArraySchema, + validateSchema, + withdrawDelegatedStakeSchema, } from "./validationSchemas"; -import { allowSpend, tokenLock } from "./shared/operations"; export class DagAccount { private m_keyTrio: KeyTrio; @@ -148,6 +152,14 @@ export class DagAccount { ); } + async getActions(actionType?: ActionType, limit?: number, searchAfter?: string, searchBefore?: string, next?: string): Promise { + const actions = await this.network.blockExplorerV2Api.getActionsByAddress(this.address, actionType, limit, searchAfter, searchBefore, next); + + if (!actions?.data?.length) return []; + + return actions.data.filter(action => action.source === this.address); + } + assertValidPrivateKey() { if (!this.m_keyTrio.privateKey) { throw new Error( @@ -176,6 +188,80 @@ export class DagAccount { return 0; } + async getActiveTokenLocksTransactions(limit?: number, searchAfter?: string, searchBefore?: string): Promise { + const activeTokenLocks: TokenLockResponse[] = []; + let next: string | undefined; + + do { + const response = await this.network.blockExplorerV2Api.getTokenLocksByAddress( + this.address, + limit, + searchAfter, + searchBefore, + next, + true, + ); + + if (response?.data) { + activeTokenLocks.push(...response.data); + } + + next = response?.meta?.next; + } while (next); + + return activeTokenLocks; + }; + + async getActiveAllowSpendsTransactions(limit?: number, searchAfter?: string, searchBefore?: string): Promise { + const activeAllowSpends: AllowSpendResponse[] = []; + let next: string | undefined; + + do { + const response = await this.network.blockExplorerV2Api.getAllowSpendsByAddress( + this.address, + limit, + searchAfter, + searchBefore, + next, + true, + ); + + if (response?.data) { + activeAllowSpends.push(...response.data); + } + + next = response?.meta?.next; + } while (next); + + return activeAllowSpends; + }; + + async getLockedBalance(): Promise { + const [tokenLocks, allowSpends] = await Promise.all([ + this.getActiveTokenLocksTransactions(), + this.getActiveAllowSpendsTransactions() + ]); + + let lockedAmount = new BigNumber(0); + + if (tokenLocks.length > 0) { + for (const tokenLock of tokenLocks) { + lockedAmount = lockedAmount.plus(new BigNumber(tokenLock.amount)); + } + } + + if (allowSpends.length > 0) { + for (const allowSpend of allowSpends) { + if (allowSpend.source === this.address) { + lockedAmount = lockedAmount.plus(new BigNumber(allowSpend.amount)); + } + } + } + + // Returns locked amount in DAG + return lockedAmount.multipliedBy(DAG_DECIMALS).toNumber(); + } + async getFeeRecommendation() { //Get last tx ref const lastRef = (await this.network.getAddressLastAcceptedTransactionRef( diff --git a/packages/dag4-wallet/src/dag-monitor.ts b/packages/dag4-wallet/src/dag-monitor.ts index 91d6743..177013f 100644 --- a/packages/dag4-wallet/src/dag-monitor.ts +++ b/packages/dag4-wallet/src/dag-monitor.ts @@ -1,7 +1,7 @@ -import {crossPlatformDi} from '@stardust-collective/dag4-core'; -import {loadBalancerApi, Transaction, PendingTx, CbTransaction, TransactionV2} from '@stardust-collective/dag4-network'; -import {DagAccount} from './dag-account'; -import {Subject} from 'rxjs'; +import { crossPlatformDi } from '@stardust-collective/dag4-core'; +import { type CbTransaction, loadBalancerApi, type PendingTx, type Transaction, type TransactionV2 } from '@stardust-collective/dag4-network'; +import { Subject } from 'rxjs'; +import { DagAccount } from './dag-account'; const TWELVE_MINUTES = 12 * 60 * 1000; @@ -57,16 +57,10 @@ export class DagMonitor { } async getLatestTransactions (address: string, limit?: number, searchAfter?: string): Promise<(Transaction | TransactionV2)[]> { - const cTxs = await this.dagAccount.networkInstance.getTransactionsByAddress(address, limit, searchAfter); + const transactions = await this.dagAccount.networkInstance.getTransactionsByAddress(address, limit, searchAfter); + const allTxs = await this.concatPendingTransactions(transactions) as (Transaction | TransactionV2)[]; - const { pendingTxs } = await this.processPendingTxs(); - const pendingTransactions = pendingTxs.map(pending => this.transformPendingToTransaction(pending)); - - if (cTxs && cTxs.length) { - return pendingTransactions.concat(cTxs); - } - - return pendingTransactions; + return allTxs } async getMemPoolFromMonitor(address?: string): Promise { @@ -111,6 +105,17 @@ export class DagMonitor { this.pollPendingTxs(); } + private async concatPendingTransactions(transactions: (Transaction | TransactionV2)[]): Promise<(Transaction | TransactionV2)[]> { + const { pendingTxs } = await this.processPendingTxs(); + const pendingTransactions: (Transaction | TransactionV2)[] = pendingTxs.map(pending => this.transformPendingToTransaction(pending)); + + if (transactions && transactions.length) { + return [...pendingTransactions, ...transactions]; + } + + return pendingTransactions; + } + private transformPendingToTransaction (pending: PendingTx): Transaction | TransactionV2 { const { hash, amount, receiver, sender, timestamp, ordinal, fee, status } = pending; const networkVersion = this.dagAccount.networkInstance.getNetworkVersion(); diff --git a/packages/dag4-wallet/src/metagraph-token-client.ts b/packages/dag4-wallet/src/metagraph-token-client.ts index c2988ac..5ac3574 100644 --- a/packages/dag4-wallet/src/metagraph-token-client.ts +++ b/packages/dag4-wallet/src/metagraph-token-client.ts @@ -1,14 +1,15 @@ -import { PostTransactionV2 } from "@stardust-collective/dag4-keystore"; import { DAG_DECIMALS } from "@stardust-collective/dag4-core"; +import { PostTransactionV2 } from "@stardust-collective/dag4-keystore"; import { - PendingTx, - TransactionReference, - MetagraphTokenNetwork, - MetagraphNetworkInfo, + ActionType, AllowSpend, AllowSpendWithCurrencyId, + MetagraphNetworkInfo, + MetagraphTokenNetwork, + PendingTx, TokenLock, TokenLockWithCurrencyId, + TransactionReference, } from "@stardust-collective/dag4-network"; import { BigNumber } from "bignumber.js"; import type { DagAccount } from "./dag-account"; @@ -41,6 +42,14 @@ class MetagraphTokenClient { ); } + async getActions(actionType?: ActionType, limit?: number, searchAfter?: string, searchBefore?: string, next?: string) { + const actions = await this.network.getActionsByAddress(this.address, actionType, limit, searchAfter, searchBefore, next); + + if (!actions?.length) return []; + + return actions.filter(action => action.source === this.address); + } + async getBalance() { return this.getBalanceFor(this.address); } @@ -57,6 +66,32 @@ class MetagraphTokenClient { return 0; } + async getLockedBalance() { + const [tokenLocks, allowSpends] = await Promise.all([ + this.network.getActiveTokenLocksTransactions(this.address), + this.network.getActiveAllowSpendsTransactions(this.address) + ]); + + let lockedAmount = new BigNumber(0); + + if (tokenLocks.length > 0) { + for (const tokenLock of tokenLocks) { + lockedAmount = lockedAmount.plus(new BigNumber(tokenLock.amount)); + } + } + + if (allowSpends.length > 0) { + for (const allowSpend of allowSpends) { + if (allowSpend.source === this.address) { + lockedAmount = lockedAmount.plus(new BigNumber(allowSpend.amount)); + } + } + } + + // Returns locked amount in DAG + return lockedAmount.multipliedBy(DAG_DECIMALS).toNumber(); + } + async getFeeRecommendation() { //Get last tx ref const lastRef = await this.network.getAddressLastAcceptedTransactionRef(