From 23460370af23515007341643f5a564526538e286 Mon Sep 17 00:00:00 2001 From: Juan Olmedo Date: Wed, 6 Aug 2025 11:38:00 -0300 Subject: [PATCH 1/4] feat: add actions and locked balance --- .../src/api/v2/block-explorer-api.ts | 52 +++++-- packages/dag4-network/src/dag-network.ts | 18 +-- packages/dag4-network/src/dto/v2/index.ts | 12 +- packages/dag4-network/src/dto/v2/staking.ts | 47 +++++++ .../dag4-network/src/dto/v2/transaction.ts | 3 + .../src/metagraph-token-network.ts | 35 +++-- packages/dag4-wallet/src/dag-account.ts | 130 +++++++++++++++--- packages/dag4-wallet/src/dag-monitor.ts | 33 +++-- .../dag4-wallet/src/metagraph-token-client.ts | 19 ++- 9 files changed, 280 insertions(+), 69 deletions(-) create mode 100644 packages/dag4-network/src/dto/v2/staking.ts 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..88fd4d6 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, + ActionV2, + AddressBalanceV2, + AllowSpendV2, BlockV2, - CurrencySnapshotV2 + CurrencySnapshotV2, + RewardTransaction, + SnapshotV2, + TokenLockV2, + TransactionV2 } from '../../dto/v2'; type HashOrOrdinal = string | number; @@ -70,12 +74,14 @@ export class BlockExplorerV2Api { limit = null, searchAfter = null, searchBefore = null, - next = null + next = null, + actionType = null } : { limit?: number, searchAfter?: string, searchBefore?: string, - next?: string + next?: string, + actionType?: ActionType }) { let params; @@ -86,6 +92,10 @@ export class BlockExplorerV2Api { params.limit = limit; } + if (actionType) { + params.transactionType = actionType; + } + // search_after, search_before and next are mutually exclusive if (searchAfter) { params.search_after = searchAfter; @@ -173,11 +183,35 @@ 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 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) { + const params = this.buildRequestParams({ limit, searchAfter, searchBefore, next }); + + return this.service.$get>(`/addresses/${address}/token-locks`, params); + } + + async getAllowSpendsByAddress(address: string, limit?: number, searchAfter?: string, searchBefore?: string, next?: string) { + const params = this.buildRequestParams({ limit, searchAfter, searchBefore, next }); + + 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..ae27f30 --- /dev/null +++ b/packages/dag4-network/src/dto/v2/staking.ts @@ -0,0 +1,47 @@ +export type TokenLockV2 = { + 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 AllowSpendV2 = { + 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 ActionV2 = { + 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..3a89f74 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 @@ -54,3 +55,5 @@ export type PostTransactionV2 = { export type PostTransactionResponseV2 = { hash: string }; + +export type DagTransaction = Transaction | TransactionV2; diff --git a/packages/dag4-network/src/metagraph-token-network.ts b/packages/dag4-network/src/metagraph-token-network.ts index 1506b3a..4e3eaf1 100644 --- a/packages/dag4-network/src/metagraph-token-network.ts +++ b/packages/dag4-network/src/metagraph-token-network.ts @@ -1,14 +1,16 @@ -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, + ActionV2, + CurrencySnapshotV2, PendingTransaction, - TransactionV2, - CurrencySnapshotV2 + PostTransactionV2, + TransactionV2 } 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 +71,21 @@ 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 getTransaction( hash: string | null ): Promise { @@ -121,4 +138,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..7ff97f8 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, + ActionV2, + AllowSpend, + AllowSpendV2, + AllowSpendWithCurrencyId, + DagNetwork, + DelegatedStake, + DelegatedStakeWithParent, globalDagNetwork, GlobalDagNetwork, - DagNetwork, + HashResponse, + MetagraphNetworkInfo, NetworkInfo, PendingTx, - TransactionReference, - MetagraphNetworkInfo, - AllowSpendWithCurrencyId, - AllowSpend, - TokenLock, - TokenLockWithCurrencyId, - DelegatedStake, - WithdrawDelegatedStake, - HashResponse, SignedDelegatedStake, SignedWithdrawDelegatedStake, - DelegatedStakeWithParent, + TokenLock, + TokenLockV2, + 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,88 @@ export class DagAccount { return 0; } + async getTokenLocksTransactions(limit?: number, searchAfter?: string, searchBefore?: string): Promise { + const allTokenLocks: TokenLockV2[] = []; + let next: string | undefined; + + do { + const response = await this.network.blockExplorerV2Api.getTokenLocksByAddress( + this.address, + limit, + searchAfter, + searchBefore, + next + ); + + if (response?.data) { + allTokenLocks.push(...response.data); + } + + next = response?.meta?.next; + } while (next); + + return allTokenLocks; + }; + + async getAllowSpendsTransactions(limit?: number, searchAfter?: string, searchBefore?: string): Promise { + const allAllowSpends: AllowSpendV2[] = []; + let next: string | undefined; + + do { + const response = await this.network.blockExplorerV2Api.getAllowSpendsByAddress( + this.address, + limit, + searchAfter, + searchBefore, + next + ); + + if (response?.data) { + allAllowSpends.push(...response.data); + } + + next = response?.meta?.next; + } while (next); + + return allAllowSpends; + }; + + async getLockedBalance(currencyId?: string): Promise { + const currency = currencyId || null; // Default to null (DAG) + + const [tokenLocks, allowSpends, latestSnapshot] = await Promise.all([ + this.getTokenLocksTransactions(), + this.getAllowSpendsTransactions(), + this.network.l0Api.getLatestSnapshot() + ]); + + let lockedAmount = 0; + + if (tokenLocks.length > 0) { + for (const tokenLock of tokenLocks) { + if (tokenLock.currencyId === currency && tokenLock.unlockedAtOrdinal === null) { + lockedAmount += tokenLock.amount; + } + } + } + + if (allowSpends.length > 0 && latestSnapshot?.value?.epochProgress !== undefined) { + const currentEpochProgress = latestSnapshot.value.epochProgress; + + for (const allowSpend of allowSpends) { + if ( + allowSpend.currencyId === currency && + allowSpend.source === this.address && + allowSpend.lastValidEpochProgress >= currentEpochProgress + ) { + lockedAmount += allowSpend.amount; + } + } + } + + return lockedAmount; + } + 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..4f8795f 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, type DagTransaction, 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; @@ -56,17 +56,11 @@ export class DagMonitor { return this.transformPendingToTransaction(tx); } - async getLatestTransactions (address: string, limit?: number, searchAfter?: string): Promise<(Transaction | TransactionV2)[]> { - const cTxs = await this.dagAccount.networkInstance.getTransactionsByAddress(address, limit, searchAfter); + async getLatestTransactions (address: string, limit?: number, searchAfter?: string): Promise { + const transactions = await this.dagAccount.networkInstance.getTransactionsByAddress(address, limit, searchAfter); + const allTxs = await this.concatPendingTransactions(transactions) as DagTransaction[]; - 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: DagTransaction[]): Promise { + const { pendingTxs } = await this.processPendingTxs(); + const pendingTransactions: DagTransaction[] = 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..2ed9a10 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); } From 7f34e066ce28c5cd82fd84148b2ad63a56203759 Mon Sep 17 00:00:00 2001 From: Juan Olmedo Date: Thu, 7 Aug 2025 13:04:57 -0300 Subject: [PATCH 2/4] refactor: filter actions by status active --- .../src/api/v2/block-explorer-api.ts | 34 ++++++++++------- packages/dag4-network/src/dto/v2/staking.ts | 6 +-- .../dag4-network/src/dto/v2/transaction.ts | 2 - .../src/metagraph-token-network.ts | 4 +- packages/dag4-wallet/src/dag-account.ts | 38 +++++++++---------- packages/dag4-wallet/src/dag-monitor.ts | 10 ++--- 6 files changed, 48 insertions(+), 46 deletions(-) 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 88fd4d6..8511d98 100644 --- a/packages/dag4-network/src/api/v2/block-explorer-api.ts +++ b/packages/dag4-network/src/api/v2/block-explorer-api.ts @@ -2,14 +2,14 @@ import { RestApi } from '@stardust-collective/dag4-core'; import { DNC } from '../../DNC'; import { ActionType, - ActionV2, + ActionResponse, AddressBalanceV2, - AllowSpendV2, + AllowSpendResponse, BlockV2, CurrencySnapshotV2, RewardTransaction, SnapshotV2, - TokenLockV2, + TokenLockResponse, TransactionV2 } from '../../dto/v2'; @@ -75,17 +75,19 @@ export class BlockExplorerV2Api { searchAfter = null, searchBefore = null, next = null, - actionType = null + actionType = null, + active = false } : { limit?: number, searchAfter?: string, searchBefore?: string, next?: string, - actionType?: ActionType + actionType?: ActionType, + active?: boolean }) { let params; - if (limit || searchAfter || searchBefore || next) { + if (limit || searchAfter || searchBefore || next || active) { params = {}; if (limit && limit > 0) { @@ -96,6 +98,10 @@ export class BlockExplorerV2Api { params.transactionType = actionType; } + if (active) { + params.active = active; + } + // search_after, search_before and next are mutually exclusive if (searchAfter) { params.search_after = searchAfter; @@ -186,7 +192,7 @@ export class BlockExplorerV2Api { 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); + return this.service.$get>(`/currency/${metagraphId}/addresses/${address}/actions`, params); } async getCurrencyTransactionsBySnapshot(metagraphId: string, hashOrOrdinal: string, limit?: number, searchAfter?: string, searchBefore?: string, next?: string) { @@ -198,19 +204,19 @@ export class BlockExplorerV2Api { 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); + return this.service.$get>(`/addresses/${address}/actions`, params); } - async getTokenLocksByAddress(address: string, limit?: number, searchAfter?: string, searchBefore?: string, next?: string) { - const params = this.buildRequestParams({ limit, searchAfter, searchBefore, next }); + 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); + return this.service.$get>(`/addresses/${address}/token-locks`, params); } - async getAllowSpendsByAddress(address: string, limit?: number, searchAfter?: string, searchBefore?: string, next?: string) { - const params = this.buildRequestParams({ limit, searchAfter, searchBefore, next }); + 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); + return this.service.$get>(`/addresses/${address}/allow-spends`, params); } } diff --git a/packages/dag4-network/src/dto/v2/staking.ts b/packages/dag4-network/src/dto/v2/staking.ts index ae27f30..81ebc0d 100644 --- a/packages/dag4-network/src/dto/v2/staking.ts +++ b/packages/dag4-network/src/dto/v2/staking.ts @@ -1,4 +1,4 @@ -export type TokenLockV2 = { +export type TokenLockResponse = { currencyId: string | null; hash: string; ordinal: number; @@ -13,7 +13,7 @@ export type TokenLockV2 = { unlockedAtOrdinal: number | null; }; -export type AllowSpendV2 = { +export type AllowSpendResponse = { currencyId: string | null; hash: string; ordinal: number; @@ -32,7 +32,7 @@ export type AllowSpendV2 = { export const Actions = ["TokenLock", "TokenUnlock", "AllowSpend", "ExpiredAllowSpend", "DelegateStakeCreate", "DelegateStakeWithdraw", "SpendTransaction", "FeeTransaction", "ExpiredSpendTransaction"] as const; export type ActionType = (typeof Actions)[number]; -export type ActionV2 = { +export type ActionResponse = { type: ActionType; currencyId: string | null; hash: string; diff --git a/packages/dag4-network/src/dto/v2/transaction.ts b/packages/dag4-network/src/dto/v2/transaction.ts index 3a89f74..dc7af34 100644 --- a/packages/dag4-network/src/dto/v2/transaction.ts +++ b/packages/dag4-network/src/dto/v2/transaction.ts @@ -55,5 +55,3 @@ export type PostTransactionV2 = { export type PostTransactionResponseV2 = { hash: string }; - -export type DagTransaction = Transaction | TransactionV2; diff --git a/packages/dag4-network/src/metagraph-token-network.ts b/packages/dag4-network/src/metagraph-token-network.ts index 4e3eaf1..a34ce62 100644 --- a/packages/dag4-network/src/metagraph-token-network.ts +++ b/packages/dag4-network/src/metagraph-token-network.ts @@ -4,7 +4,7 @@ import { MetagraphTokenL1Api } from "./api/metagraph-token/l1-api"; import { BlockExplorerV2Api } from './api/v2/block-explorer-api'; import { ActionType, - ActionV2, + ActionResponse, CurrencySnapshotV2, PendingTransaction, PostTransactionV2, @@ -78,7 +78,7 @@ class MetagraphTokenNetwork { searchAfter?: string, searchBefore?: string, next?: string - ): Promise { + ): Promise { const actions = await this.beApi.getCurrencyActionsByAddress(this.connectedNetwork.metagraphId, address, actionType, limit, searchAfter, searchBefore, next); if (!actions?.data?.length) return []; diff --git a/packages/dag4-wallet/src/dag-account.ts b/packages/dag4-wallet/src/dag-account.ts index 7ff97f8..ef8e0cb 100644 --- a/packages/dag4-wallet/src/dag-account.ts +++ b/packages/dag4-wallet/src/dag-account.ts @@ -7,9 +7,9 @@ import { } from "@stardust-collective/dag4-keystore"; import { ActionType, - ActionV2, + ActionResponse, AllowSpend, - AllowSpendV2, + AllowSpendResponse, AllowSpendWithCurrencyId, DagNetwork, DelegatedStake, @@ -23,7 +23,7 @@ import { SignedDelegatedStake, SignedWithdrawDelegatedStake, TokenLock, - TokenLockV2, + TokenLockResponse, TokenLockWithCurrencyId, TransactionReference, WithdrawDelegatedStake @@ -152,7 +152,7 @@ export class DagAccount { ); } - async getActions(actionType?: ActionType, limit?: number, searchAfter?: string, searchBefore?: string, next?: string): Promise { + 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 []; @@ -188,8 +188,8 @@ export class DagAccount { return 0; } - async getTokenLocksTransactions(limit?: number, searchAfter?: string, searchBefore?: string): Promise { - const allTokenLocks: TokenLockV2[] = []; + async getActiveTokenLocksTransactions(limit?: number, searchAfter?: string, searchBefore?: string): Promise { + const allTokenLocks: TokenLockResponse[] = []; let next: string | undefined; do { @@ -198,7 +198,8 @@ export class DagAccount { limit, searchAfter, searchBefore, - next + next, + true, ); if (response?.data) { @@ -211,8 +212,8 @@ export class DagAccount { return allTokenLocks; }; - async getAllowSpendsTransactions(limit?: number, searchAfter?: string, searchBefore?: string): Promise { - const allAllowSpends: AllowSpendV2[] = []; + async getActiveAllowSpendsTransactions(limit?: number, searchAfter?: string, searchBefore?: string): Promise { + const allAllowSpends: AllowSpendResponse[] = []; let next: string | undefined; do { @@ -221,7 +222,8 @@ export class DagAccount { limit, searchAfter, searchBefore, - next + next, + true, ); if (response?.data) { @@ -237,30 +239,26 @@ export class DagAccount { async getLockedBalance(currencyId?: string): Promise { const currency = currencyId || null; // Default to null (DAG) - const [tokenLocks, allowSpends, latestSnapshot] = await Promise.all([ - this.getTokenLocksTransactions(), - this.getAllowSpendsTransactions(), - this.network.l0Api.getLatestSnapshot() + const [tokenLocks, allowSpends] = await Promise.all([ + this.getActiveTokenLocksTransactions(), + this.getActiveAllowSpendsTransactions() ]); let lockedAmount = 0; if (tokenLocks.length > 0) { for (const tokenLock of tokenLocks) { - if (tokenLock.currencyId === currency && tokenLock.unlockedAtOrdinal === null) { + if (tokenLock.currencyId === currency) { lockedAmount += tokenLock.amount; } } } - if (allowSpends.length > 0 && latestSnapshot?.value?.epochProgress !== undefined) { - const currentEpochProgress = latestSnapshot.value.epochProgress; - + if (allowSpends.length > 0) { for (const allowSpend of allowSpends) { if ( allowSpend.currencyId === currency && - allowSpend.source === this.address && - allowSpend.lastValidEpochProgress >= currentEpochProgress + allowSpend.source === this.address ) { lockedAmount += allowSpend.amount; } diff --git a/packages/dag4-wallet/src/dag-monitor.ts b/packages/dag4-wallet/src/dag-monitor.ts index 4f8795f..177013f 100644 --- a/packages/dag4-wallet/src/dag-monitor.ts +++ b/packages/dag4-wallet/src/dag-monitor.ts @@ -1,5 +1,5 @@ import { crossPlatformDi } from '@stardust-collective/dag4-core'; -import { type CbTransaction, type DagTransaction, loadBalancerApi, type PendingTx, type Transaction, type TransactionV2 } from '@stardust-collective/dag4-network'; +import { type CbTransaction, loadBalancerApi, type PendingTx, type Transaction, type TransactionV2 } from '@stardust-collective/dag4-network'; import { Subject } from 'rxjs'; import { DagAccount } from './dag-account'; @@ -56,9 +56,9 @@ export class DagMonitor { return this.transformPendingToTransaction(tx); } - async getLatestTransactions (address: string, limit?: number, searchAfter?: string): Promise { + async getLatestTransactions (address: string, limit?: number, searchAfter?: string): Promise<(Transaction | TransactionV2)[]> { const transactions = await this.dagAccount.networkInstance.getTransactionsByAddress(address, limit, searchAfter); - const allTxs = await this.concatPendingTransactions(transactions) as DagTransaction[]; + const allTxs = await this.concatPendingTransactions(transactions) as (Transaction | TransactionV2)[]; return allTxs } @@ -105,9 +105,9 @@ export class DagMonitor { this.pollPendingTxs(); } - private async concatPendingTransactions(transactions: DagTransaction[]): Promise { + private async concatPendingTransactions(transactions: (Transaction | TransactionV2)[]): Promise<(Transaction | TransactionV2)[]> { const { pendingTxs } = await this.processPendingTxs(); - const pendingTransactions: DagTransaction[] = pendingTxs.map(pending => this.transformPendingToTransaction(pending)); + const pendingTransactions: (Transaction | TransactionV2)[] = pendingTxs.map(pending => this.transformPendingToTransaction(pending)); if (transactions && transactions.length) { return [...pendingTransactions, ...transactions]; From f3ee8bd482a6e059161c893c02fbdfbf5a9173de Mon Sep 17 00:00:00 2001 From: Juan Olmedo Date: Fri, 8 Aug 2025 14:39:47 -0300 Subject: [PATCH 3/4] feat: locked balance on metagraphs --- .../src/api/v2/block-explorer-api.ts | 12 +++++ .../src/metagraph-token-network.ts | 54 ++++++++++++++++++- packages/dag4-wallet/src/dag-account.ts | 25 ++++----- .../dag4-wallet/src/metagraph-token-client.ts | 25 +++++++++ 4 files changed, 99 insertions(+), 17 deletions(-) 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 8511d98..c8814d6 100644 --- a/packages/dag4-network/src/api/v2/block-explorer-api.ts +++ b/packages/dag4-network/src/api/v2/block-explorer-api.ts @@ -201,6 +201,18 @@ export class BlockExplorerV2Api { 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 }); diff --git a/packages/dag4-network/src/metagraph-token-network.ts b/packages/dag4-network/src/metagraph-token-network.ts index a34ce62..30d2ac7 100644 --- a/packages/dag4-network/src/metagraph-token-network.ts +++ b/packages/dag4-network/src/metagraph-token-network.ts @@ -8,7 +8,9 @@ import { CurrencySnapshotV2, PendingTransaction, PostTransactionV2, - TransactionV2 + TransactionV2, + AllowSpendResponse, + TokenLockResponse } from "./dto/v2"; import { MetagraphNetworkInfo } from "./types/network-info"; @@ -86,6 +88,56 @@ class MetagraphTokenNetwork { 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 { diff --git a/packages/dag4-wallet/src/dag-account.ts b/packages/dag4-wallet/src/dag-account.ts index ef8e0cb..8900767 100644 --- a/packages/dag4-wallet/src/dag-account.ts +++ b/packages/dag4-wallet/src/dag-account.ts @@ -189,7 +189,7 @@ export class DagAccount { } async getActiveTokenLocksTransactions(limit?: number, searchAfter?: string, searchBefore?: string): Promise { - const allTokenLocks: TokenLockResponse[] = []; + const activeTokenLocks: TokenLockResponse[] = []; let next: string | undefined; do { @@ -203,17 +203,17 @@ export class DagAccount { ); if (response?.data) { - allTokenLocks.push(...response.data); + activeTokenLocks.push(...response.data); } next = response?.meta?.next; } while (next); - return allTokenLocks; + return activeTokenLocks; }; async getActiveAllowSpendsTransactions(limit?: number, searchAfter?: string, searchBefore?: string): Promise { - const allAllowSpends: AllowSpendResponse[] = []; + const activeAllowSpends: AllowSpendResponse[] = []; let next: string | undefined; do { @@ -227,18 +227,16 @@ export class DagAccount { ); if (response?.data) { - allAllowSpends.push(...response.data); + activeAllowSpends.push(...response.data); } next = response?.meta?.next; } while (next); - return allAllowSpends; + return activeAllowSpends; }; - async getLockedBalance(currencyId?: string): Promise { - const currency = currencyId || null; // Default to null (DAG) - + async getLockedBalance(): Promise { const [tokenLocks, allowSpends] = await Promise.all([ this.getActiveTokenLocksTransactions(), this.getActiveAllowSpendsTransactions() @@ -248,18 +246,13 @@ export class DagAccount { if (tokenLocks.length > 0) { for (const tokenLock of tokenLocks) { - if (tokenLock.currencyId === currency) { - lockedAmount += tokenLock.amount; - } + lockedAmount += tokenLock.amount; } } if (allowSpends.length > 0) { for (const allowSpend of allowSpends) { - if ( - allowSpend.currencyId === currency && - allowSpend.source === this.address - ) { + if (allowSpend.source === this.address) { lockedAmount += allowSpend.amount; } } diff --git a/packages/dag4-wallet/src/metagraph-token-client.ts b/packages/dag4-wallet/src/metagraph-token-client.ts index 2ed9a10..36a8026 100644 --- a/packages/dag4-wallet/src/metagraph-token-client.ts +++ b/packages/dag4-wallet/src/metagraph-token-client.ts @@ -66,6 +66,31 @@ class MetagraphTokenClient { return 0; } + async getLockedBalance() { + const [tokenLocks, allowSpends] = await Promise.all([ + this.network.getActiveTokenLocksTransactions(this.address), + this.network.getActiveAllowSpendsTransactions(this.address) + ]); + + let lockedAmount = 0; + + if (tokenLocks.length > 0) { + for (const tokenLock of tokenLocks) { + lockedAmount += tokenLock.amount; + } + } + + if (allowSpends.length > 0) { + for (const allowSpend of allowSpends) { + if (allowSpend.source === this.address) { + lockedAmount += allowSpend.amount; + } + } + } + + return lockedAmount; + } + async getFeeRecommendation() { //Get last tx ref const lastRef = await this.network.getAddressLastAcceptedTransactionRef( From fdbd8dc577c069f00c40abe896182b5275e70f0e Mon Sep 17 00:00:00 2001 From: Juan Olmedo Date: Mon, 11 Aug 2025 12:07:09 -0300 Subject: [PATCH 4/4] fix: BigNumber for safe math operations --- packages/dag4-wallet/src/dag-account.ts | 9 +++++---- packages/dag4-wallet/src/metagraph-token-client.ts | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/dag4-wallet/src/dag-account.ts b/packages/dag4-wallet/src/dag-account.ts index 8900767..f8d6768 100644 --- a/packages/dag4-wallet/src/dag-account.ts +++ b/packages/dag4-wallet/src/dag-account.ts @@ -242,23 +242,24 @@ export class DagAccount { this.getActiveAllowSpendsTransactions() ]); - let lockedAmount = 0; + let lockedAmount = new BigNumber(0); if (tokenLocks.length > 0) { for (const tokenLock of tokenLocks) { - lockedAmount += tokenLock.amount; + lockedAmount = lockedAmount.plus(new BigNumber(tokenLock.amount)); } } if (allowSpends.length > 0) { for (const allowSpend of allowSpends) { if (allowSpend.source === this.address) { - lockedAmount += allowSpend.amount; + lockedAmount = lockedAmount.plus(new BigNumber(allowSpend.amount)); } } } - return lockedAmount; + // Returns locked amount in DAG + return lockedAmount.multipliedBy(DAG_DECIMALS).toNumber(); } async getFeeRecommendation() { diff --git a/packages/dag4-wallet/src/metagraph-token-client.ts b/packages/dag4-wallet/src/metagraph-token-client.ts index 36a8026..5ac3574 100644 --- a/packages/dag4-wallet/src/metagraph-token-client.ts +++ b/packages/dag4-wallet/src/metagraph-token-client.ts @@ -72,23 +72,24 @@ class MetagraphTokenClient { this.network.getActiveAllowSpendsTransactions(this.address) ]); - let lockedAmount = 0; + let lockedAmount = new BigNumber(0); if (tokenLocks.length > 0) { for (const tokenLock of tokenLocks) { - lockedAmount += tokenLock.amount; + lockedAmount = lockedAmount.plus(new BigNumber(tokenLock.amount)); } } if (allowSpends.length > 0) { for (const allowSpend of allowSpends) { if (allowSpend.source === this.address) { - lockedAmount += allowSpend.amount; + lockedAmount = lockedAmount.plus(new BigNumber(allowSpend.amount)); } } } - return lockedAmount; + // Returns locked amount in DAG + return lockedAmount.multipliedBy(DAG_DECIMALS).toNumber(); } async getFeeRecommendation() {