Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ out
*.env

CLAUDE.md
GEMINI.md

# Orderbook example temporary files
examples/orderbook/.query_id
38 changes: 38 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1066,6 +1066,44 @@ await tx.wait();
console.log(`Withdrawal claimed! Tx: ${tx.hash}`);
```

### `client.getHistory(bridgeIdentifier: string, walletAddress: string, limit?: number, offset?: number): Promise<BridgeHistory[]>`
Retrieves the transaction history for a wallet on a specific bridge. This method is provided by the base action handler (`client.loadAction()`) and also exposed directly on the client instance (`client.getHistory(...)`) for convenience.

#### Parameters
- `bridgeIdentifier: string` - The unique identifier of the bridge (e.g., "hoodi_tt2")
- `walletAddress: string` - The wallet address to query
- `limit?: number` - Max number of records to return (optional, default 20)
- `offset?: number` - Number of records to skip (optional, default 0)

#### Returns
- `Promise<BridgeHistory[]>` - Array of history records

#### Example
```typescript
const history = await client.getHistory("hoodi_tt2", "0x...", 10, 0);

for (const rec of history) {
console.log(`${rec.type} - Amount: ${rec.amount} - Status: ${rec.status}`);
}
```

#### `BridgeHistory` Type

```typescript
interface BridgeHistory {
type: string; // "deposit", "withdrawal", "transfer"
amount: string; // NUMERIC(78,0) as string
from_address: string | null; // Sender address (hex)
to_address: string; // Recipient address (hex)
internal_tx_hash: string | null; // Kwil TX hash (base64)
external_tx_hash: string | null; // Ethereum TX hash (base64)
status: string; // "completed", "pending_epoch", "claimed"
block_height: number; // Kwil block height
block_timestamp: number; // Kwil block timestamp
external_block_height: number | null; // Ethereum block height
}
```

### `action.listWalletRewards(bridgeIdentifier: string, wallet: string, withPending: boolean): Promise<any[]>`

Lists wallet rewards for a specific bridge instance. This is a low-level method that directly accesses the bridge extension namespace.
Expand Down
118 changes: 118 additions & 0 deletions examples/history_example/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { NodeTNClient } from "../../src/index.node";
import { Wallet } from "ethers";
import dotenv from "dotenv";

dotenv.config();

async function main() {
const privateKey = process.env.TN_PRIVATE_KEY || "0000000000000000000000000000000000000000000000000000000000000001";
const endpoint = process.env.TN_GATEWAY_URL || "https://gateway.testnet.truf.network";
const wallet = new Wallet(privateKey);

const client = new NodeTNClient({
endpoint: endpoint,
signerInfo: {
address: wallet.address,
signer: wallet,
},
chainId: "testnet-v1",
timeout: 30000,
});

console.log("🔄 Transaction History Demo (JS)");
console.log("===============================");
console.log(`Endpoint: ${endpoint}`);
console.log(`Wallet: ${wallet.address}\n`);

const bridgeId = "hoodi_tt2";
const targetWallet = "0xc11Ff6d3cC60823EcDCAB1089F1A4336053851EF";
const limit = 10;
const offset = 0;

console.log(`📋 Fetching history for bridge '${bridgeId}'...`);
console.log(` Wallet: ${targetWallet}`);
console.log(` Limit: ${limit}`);
console.log(` Offset: ${offset}`);
console.log("-".repeat(60));

try {
const history = await client.getHistory(bridgeId, targetWallet, limit, offset);

if (history.length === 0) {
console.log("No history records found.");
return;
}

// Helper to format/shorten
const formatShort = (val: string | null): string => {
if (!val) return "null";

let s = val;
// Check if it's Base64 (Kwil default) or already Hex
if (!s.startsWith("0x")) {
try {
const buffer = Buffer.from(s, 'base64');
// Check if it looks like a hash/address (20 or 32 bytes)
if (buffer.length === 20 || buffer.length === 32) {
s = "0x" + buffer.toString('hex');
}
} catch (e) {
// Not base64, keep as is
}
}

if (s.length > 12) {
return s.substring(0, 10) + "...";
}
return s;
};

// Print Header
console.log(
"TYPE".padEnd(12) +
"AMOUNT".padEnd(22) +
"FROM".padEnd(14) +
"TO".padEnd(14) +
"INT TX".padEnd(14) +
"EXT TX".padEnd(14) +
"STATUS".padEnd(12) +
"BLOCK".padEnd(8) +
"TIMESTAMP"
);
console.log(
"-".repeat(12) + " " +
"-".repeat(22) + " " +
"-".repeat(14) + " " +
"-".repeat(14) + " " +
"-".repeat(14) + " " +
"-".repeat(14) + " " +
"-".repeat(12) + " " +
"-".repeat(8) + " " +
"-".repeat(20)
);

for (const rec of history) {
const date = new Date(rec.block_timestamp * 1000).toISOString();

console.log(
rec.type.padEnd(12) +
rec.amount.toString().padEnd(22) +
formatShort(rec.from_address).padEnd(14) +
formatShort(rec.to_address).padEnd(14) +
formatShort(rec.internal_tx_hash).padEnd(14) +
formatShort(rec.external_tx_hash).padEnd(14) +
rec.status.padEnd(12) +
rec.block_height.toString().padEnd(8) +
date
);
}

console.log(`\n✅ Successfully retrieved ${history.length} records.`);
console.log("\nNote: 'completed' means credited (deposits) or ready to claim (withdrawals). 'claimed' means withdrawn on Ethereum.");

} catch (error) {
console.error("❌ Failed to fetch history:", error);
}
}

main();
22 changes: 21 additions & 1 deletion src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { deleteStream } from "../contracts-api/deleteStream";
import { PrimitiveAction } from "../contracts-api/primitiveAction";
import { Action, ListMetadataByHeightParams, MetadataQueryResult } from "../contracts-api/action";
import { StreamType } from "../contracts-api/contractValues";
import { WithdrawalProof } from "../types/bridge";
import { WithdrawalProof, BridgeHistory } from "../types/bridge";
import { StreamLocator, TNStream } from "../types/stream";
import { EthereumAddress } from "../util/EthereumAddress";
import { StreamId } from "../util/StreamId";
Expand Down Expand Up @@ -388,6 +388,26 @@ export abstract class BaseTNClient<T extends EnvironmentType> {
return action.getWithdrawalProof(bridgeIdentifier, walletAddress);
}

/**
* Retrieves the transaction history for a wallet on a specific bridge.
*
* @param bridgeIdentifier - The name of the bridge instance (e.g., "hoodi_tt", "sepolia_bridge")
* @param walletAddress - The wallet address to query
* @param limit - Max number of records to return (default 20)
* @param offset - Number of records to skip (default 0)
* @returns Promise resolving to an array of BridgeHistory records
*
* @example
* ```typescript
* const history = await client.getHistory("sepolia_bridge", "0x123...");
* console.log(history);
* ```
*/
async getHistory(bridgeIdentifier: string, walletAddress: string, limit: number = 20, offset: number = 0): Promise<BridgeHistory[]> {
const action = this.loadAction();
return action.getHistory(bridgeIdentifier, walletAddress, limit, offset);
}

/**
* Gets taxonomies for specific streams in batch.
* High-level wrapper for ComposedAction.getTaxonomiesForStreams()
Expand Down
80 changes: 80 additions & 0 deletions src/contracts-api/action.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Action } from "./action";
import { BridgeHistory } from "../types/bridge";
import { KwilSigner, NodeKwil } from "@trufnetwork/kwil-js";
import { Either } from "monads-io";
import { vi, describe, it, expect } from "vitest";

describe("Action", () => {
// Mock dependencies
const mockKwil = {
call: vi.fn(),
} as unknown as NodeKwil;

const mockSigner = {
signatureType: "secp256k1_ep",
} as unknown as KwilSigner;

it("should call get_history with correct parameters", async () => {
const action = new Action(mockKwil, mockSigner);

// Spy on the 'call' method of the action instance
const callSpy = vi.spyOn(action as any, "call");

const mockHistory: BridgeHistory[] = [{
type: "deposit",
amount: "100",
from_address: null,
to_address: "0x123",
internal_tx_hash: null,
external_tx_hash: "0xabc",
status: "completed",
block_height: 10,
block_timestamp: 1000,
external_block_height: 5
}];

callSpy.mockResolvedValue(Either.right(mockHistory));

const result = await action.getHistory("sepolia_bridge", "0x123", 10, 5);

expect(callSpy).toHaveBeenCalledWith(
"sepolia_bridge_get_history",
{
$wallet_address: "0x123",
$limit: 10,
$offset: 5
}
);

expect(result).toEqual(mockHistory);
});

it("should handle null results from get_history", async () => {
const action = new Action(mockKwil, mockSigner);
const callSpy = vi.spyOn(action as any, "call");

callSpy.mockResolvedValue(Either.right(null));

const result = await action.getHistory("bridge", "0x123");

expect(result).toEqual([]);
});

it("should use default parameters for getHistory", async () => {
const action = new Action(mockKwil, mockSigner);
const callSpy = vi.spyOn(action as any, "call");

callSpy.mockResolvedValue(Either.right([]));

await action.getHistory("bridge", "0x123");

expect(callSpy).toHaveBeenCalledWith(
"bridge_get_history",
{
$wallet_address: "0x123",
$limit: 20,
$offset: 0
}
);
});
});
33 changes: 32 additions & 1 deletion src/contracts-api/action.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {KwilSigner, NodeKwil, WebKwil, Types} from "@trufnetwork/kwil-js";
import { Either } from "monads-io";
import { WithdrawalProof } from "../types/bridge";
import { WithdrawalProof, BridgeHistory } from "../types/bridge";
import { DateString } from "../types/other";
import { StreamLocator } from "../types/stream";
import { CacheAwareResponse, GetRecordOptions, GetIndexOptions, GetIndexChangeOptions, GetFirstRecordOptions } from "../types/cache";
Expand Down Expand Up @@ -1147,4 +1147,35 @@ export class Action {
})
.throw();
}

/**
* Retrieves the unified transaction history for a wallet on a specific bridge.
*
* @param bridgeIdentifier - The name of the bridge instance (e.g., "hoodi_tt", "sepolia_bridge")
* @param walletAddress - The wallet address to query
* @param limit - Max number of records to return (default 20)
* @param offset - Number of records to skip (default 0)
* @returns Array of BridgeHistory records
*/
public async getHistory(
bridgeIdentifier: string,
walletAddress: string,
limit: number = 20,
offset: number = 0
): Promise<BridgeHistory[]> {
const result = await this.call<BridgeHistory[]>(
`${bridgeIdentifier}_get_history`,
{
$wallet_address: walletAddress,
$limit: limit,
$offset: offset
}
);

return result
.mapRight((rows) => {
return rows || [];
})
.throw();
}
}
55 changes: 55 additions & 0 deletions src/types/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,58 @@ export interface WithdrawalProof {
*/
signatures: string[];
}

/**
* Transaction history record from the bridge extension.
*/
export interface BridgeHistory {
/**
* The type of transaction ('deposit', 'withdrawal', 'transfer')
*/
type: string;

/**
* The amount transferred/deposited/withdrawn
*/
amount: string;

/**
* The sender address (null for deposits)
*/
from_address: string | null;

/**
* The recipient address
*/
to_address: string;

/**
* The Kwil transaction hash (null for deposits, if not linked)
*/
internal_tx_hash: string | null;

/**
* The external transaction hash (null for internal transfers)
*/
external_tx_hash: string | null;

/**
* The status of the transaction ('completed', 'pending_epoch', 'claimed')
*/
status: string;

/**
* The Kwil block height
*/
block_height: number;

/**
* The timestamp of the block (Unix timestamp)
*/
block_timestamp: number;

/**
* The external block height (if applicable)
*/
external_block_height: number | null;
}
Loading
Loading