Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,8 @@ hiero-cli.config.*.local.json
#AI agents
.claude
CLAUDE.md
swap-quote.md
swap-HBAR-token.md


pnpm-lock.yaml
29 changes: 29 additions & 0 deletions my-subgraph/abis/IGreeter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[
{
"inputs": [{"internalType": "string", "name": "_greeting", "type": "string"}],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{"indexed": false, "internalType": "string", "name": "_greeting", "type": "string"}
],
"name": "GreetingSet",
"type": "event"
},
{
"inputs": [],
"name": "greet",
"outputs": [{"internalType": "string", "name": "", "type": "string"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{"internalType": "string", "name": "_greeting", "type": "string"}],
"name": "setGreeting",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
4 changes: 4 additions & 0 deletions my-subgraph/config/testnet.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"startBlock": "1",
"Greeter": "0x0000000000000000000000000000000000000000"
}
42 changes: 42 additions & 0 deletions my-subgraph/graph-node/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
version: '3'
services:
graph-node:
image: graphprotocol/graph-node:v0.27.0
ports:
- '8000:8000'
- '8001:8001'
- '8020:8020'
- '8030:8030'
- '8040:8040'
depends_on:
- ipfs
- postgres
extra_hosts:
- host.docker.internal:host-gateway
environment:
postgres_host: postgres
postgres_user: 'graph-node'
postgres_pass: 'let-me-in'
postgres_db: 'graph-node'
ipfs: 'ipfs:5001'
ethereum: 'testnet:https://testnet.hashio.io/api'
GRAPH_LOG: info
GRAPH_ETHEREUM_GENESIS_BLOCK_NUMBER: 1
ipfs:
image: ipfs/go-ipfs:v0.10.0
ports:
- '5001:5001'
volumes:
- ./data/ipfs:/data/ipfs
postgres:
image: postgres
ports:
- '5432:5432'
command: ['postgres', '-cshared_preload_libraries=pg_stat_statements']
environment:
POSTGRES_USER: 'graph-node'
POSTGRES_PASSWORD: 'let-me-in'
POSTGRES_DB: 'graph-node'
PGDATA: '/data/postgres'
volumes:
- ./data/postgres:/var/lib/postgresql/data
17 changes: 17 additions & 0 deletions my-subgraph/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "hedera-subgraph-greeter",
"version": "1.0.0",
"description": "Hedera subgraph on testnet - scaffolded by hiero-cli",
"scripts": {
"codegen": "graph codegen",
"build": "graph build",
"create-local": "graph create --node http://localhost:8020/ Greeter",
"deploy-local": "graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 Greeter",
"graph-node": "docker-compose -f ./graph-node/docker-compose.yaml up -d",
"graph-node-down": "docker-compose -f ./graph-node/docker-compose.yaml down"
},
"dependencies": {
"@graphprotocol/graph-cli": "0.33.0",
"@graphprotocol/graph-ts": "0.27.0"
}
}
4 changes: 4 additions & 0 deletions my-subgraph/schema.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
type Greeting @entity {
id: ID!
currentGreeting: String!
}
18 changes: 18 additions & 0 deletions my-subgraph/src/mappings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Hedera Subgraph Example - Hedera testnet
* Scaffolded by hiero-cli subgraph plugin.
*/

import { GreetingSet } from '../generated/Greeter/IGreeter';
import { Greeting } from '../generated/schema';

export function handleGreetingSet(event: GreetingSet): void {
let entity = Greeting.load(event.transaction.hash.toHexString());

if (!entity) {
entity = new Greeting(event.transaction.hash.toHex());
}

entity.currentGreeting = event.params._greeting;
entity.save();
}
26 changes: 26 additions & 0 deletions my-subgraph/subgraph.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
specVersion: 0.0.4
description: Graph for Greeter contracts on Hedera testnet
repository: https://github.com/hashgraph/hedera-subgraph-example
schema:
file: ./schema.graphql
dataSources:
- kind: ethereum/contract
name: Greeter
network: testnet
source:
address: "0x0000000000000000000000000000000000000000"
abi: IGreeter
startBlock: 1
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
entities:
- Greeting
abis:
- name: IGreeter
file: ./abis/IGreeter.json
eventHandlers:
- event: GreetingSet(string)
handler: handleGreetingSet
file: ./src/mappings.ts
4 changes: 4 additions & 0 deletions payments.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
to,amount
0.0.100,1.5
0.0.4530,2
0.0.95215,500t
29 changes: 17 additions & 12 deletions src/core/plugins/plugin-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,30 +72,35 @@ export class PluginManager {
*/
initializePluginState(defaultState: PluginManifest[]): PluginStateEntry[] {
const existingEntries = this.pluginManagement.listPlugins();
const existingNames = new Set(existingEntries.map((e) => e.name));

if (existingEntries.length === 0) {
this.logger.info(
'[PLUGIN-MANAGEMENT] Initializing default plugin state (first run)...',
);
}

const initialState: PluginStateEntry[] = defaultState.map((manifest) => {
const pluginName = manifest.name;

return {
name: pluginName,
// Add any default plugins not yet in state (e.g. newly added plugins)
const newEntries: PluginStateEntry[] = [];
for (const manifest of defaultState) {
if (!existingNames.has(manifest.name)) {
const entry: PluginStateEntry = {
name: manifest.name,
enabled: true,
description: manifest.description,
};
});

for (const plugin of initialState) {
this.pluginManagement.savePluginState(plugin);
this.pluginManagement.savePluginState(entry);
newEntries.push(entry);
existingNames.add(manifest.name);
}

return initialState;
}
if (newEntries.length > 0) {
this.logger.info(
`[PLUGIN-MANAGEMENT] Registered ${newEntries.length} new default plugin(s): ${newEntries.map((e) => e.name).join(', ')}`,
);
}

return existingEntries;
return existingEntries.concat(newEntries);
}

/**
Expand Down
22 changes: 17 additions & 5 deletions src/core/schemas/common-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -549,12 +549,24 @@ export const TokenTypeSchema = z
* - keypair input → { type: 'keypair', accountId: string, privateKey: string }
* The keyType must be fetched from mirror node when keypair is provided
*/
const KEY_OR_ALIAS_REFINE_MESSAGE =
'Operator must be an account alias (e.g. my-operator) or accountId:privateKey (e.g. 0.0.123:302e02...). Account ID alone (e.g. 0.0.7982140) is not valid.';

export const KeyOrAccountAliasSchema = z
.union([AccountIdWithPrivateKeySchema, AccountNameSchema])
.transform((val) =>
typeof val === 'string'
? { type: 'alias' as const, alias: val }
: { type: 'keypair' as const, ...val },
.string()
.trim()
.min(1, 'Operator cannot be empty')
.refine((val) => !(/^0\.0\.[1-9]\d*$/.test(val) && !val.includes(':')), {
message: KEY_OR_ALIAS_REFINE_MESSAGE,
})
.pipe(
z
.union([AccountIdWithPrivateKeySchema, AccountNameSchema])
.transform((val) =>
typeof val === 'string'
? { type: 'alias' as const, alias: val }
: { type: 'keypair' as const, ...val },
),
)
.describe(
'Account ID with private key in format {accountId}:{private_key} or account name/alias',
Expand Down
16 changes: 14 additions & 2 deletions src/core/services/contract-query/contract-query-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,21 @@ export class ContractQueryServiceImpl implements ContractQueryService {
this.logger.info(
`Calling contract ${params.contractIdOrEvmAddress} "${params.functionName}" function on mirror node`,
);
// Normalize to lowercase hex; some mirror node validators require it
const toHex = (
contractEvmAddress.startsWith('0x')
? contractEvmAddress
: `0x${contractEvmAddress}`
).toLowerCase();
const dataHex = (data.startsWith('0x') ? data : `0x${data}`).toLowerCase();
// Mirror node may require "from"; use zero address for read-only queries (same as Hedera SDK behavior when sender is unset)
const fromHex = '0x0000000000000000000000000000000000000000';
const response = await this.mirrorService.postContractCall({
to: contractEvmAddress,
data: data,
block: 'latest',
from: fromHex,
to: toHex,
data: dataHex,
gas: 2_000_000,
});

if (!response || !response.result) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ const TEST_TOPIC_ID = '0.0.3000';
const TEST_TX_ID = '0.0.1234-1700000000-000000000';

// Network URLs
const TESTNET_URL = 'https://testnet.mirrornode.hedera.com/api/v1';
const TESTNET_URL =
'https://testnet.hedera.validationcloud.io/v1/MtxToj_lTWpHFpYDHBw33G3M5Qs4rBJr9nK39V07dbQ/api/v1';
const MAINNET_URL = 'https://mainnet-public.mirrornode.hedera.com/api/v1';

// Timestamps & Values
Expand Down
15 changes: 14 additions & 1 deletion src/core/services/mirrornode/hedera-mirrornode-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,21 @@ export class HederaMirrornodeServiceDefaultImpl implements HederaMirrornodeServi
});

if (!response.ok) {
const body = await response.text();
let detail = body;
try {
const json = JSON.parse(body) as {
_status?: { messages?: Array<{ message?: string }> };
};
const messages = json._status?.messages
?.map((m) => m.message)
.filter(Boolean);
if (messages?.length) detail = messages.join('; ');
} catch {
// use raw body if not JSON
}
throw new Error(
`Failed to call contract via mirror node: ${response.status} ${response.statusText}`,
`Failed to call contract via mirror node: ${response.status} ${response.statusText}${detail ? ` - ${detail}` : ''}`,
);
}

Expand Down
5 changes: 4 additions & 1 deletion src/core/services/mirrornode/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ export const NetworkToBaseUrl = new Map<SupportedNetwork, string>([
SupportedNetwork.MAINNET,
'https://mainnet-public.mirrornode.hedera.com/api/v1',
],
[SupportedNetwork.TESTNET, 'https://testnet.mirrornode.hedera.com/api/v1'],
[
SupportedNetwork.TESTNET,
'https://testnet.hedera.validationcloud.io/v1/MtxToj_lTWpHFpYDHBw33G3M5Qs4rBJr9nK39V07dbQ/api/v1',
],
[
SupportedNetwork.PREVIEWNET,
'https://previewnet.mirrornode.hedera.com/api/v1',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ describe('NetworkServiceImpl', () => {
expect(config.name).toBe(NETWORK_TESTNET);
expect(config.rpcUrl).toBe('https://testnet.hashio.io/api');
expect(config.mirrorNodeUrl).toBe(
'https://testnet.mirrornode.hedera.com/api/v1',
'https://testnet.hedera.validationcloud.io/v1/MtxToj_lTWpHFpYDHBw33G3M5Qs4rBJr9nK39V07dbQ/api/v1',
);
expect(config.chainId).toBe('0x128');
expect(config.explorerUrl).toBe(`https://hashscan.io/${NETWORK_TESTNET}`);
Expand Down
3 changes: 2 additions & 1 deletion src/core/services/network/network.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export const DEFAULT_NETWORKS: Record<string, DefaultNetworkConfig> = {
},
testnet: {
rpcUrl: 'https://testnet.hashio.io/api',
mirrorNodeUrl: 'https://testnet.mirrornode.hedera.com/api/v1',
mirrorNodeUrl:
'https://testnet.hedera.validationcloud.io/v1/MtxToj_lTWpHFpYDHBw33G3M5Qs4rBJr9nK39V07dbQ/api/v1',
},
previewnet: {
rpcUrl: 'https://previewnet.hashio.io/api',
Expand Down
6 changes: 6 additions & 0 deletions src/core/shared/config/cli-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import credentialsPluginManifest from '@/plugins/credentials/manifest';
import hbarPluginManifest from '@/plugins/hbar/manifest';
import networkPluginManifest from '@/plugins/network/manifest';
import pluginManagementManifest from '@/plugins/plugin-management/manifest';
import saucerswapPluginManifest from '@/plugins/saucerswap/manifest';
import splitPaymentsPluginManifest from '@/plugins/split-payments/manifest';
import subgraphPluginManifest from '@/plugins/subgraph/manifest';
import tokenPluginManifest from '@/plugins/token/manifest';
import topicPluginManifest from '@/plugins/topic/manifest';

Expand Down Expand Up @@ -44,6 +47,9 @@ export const DEFAULT_PLUGIN_STATE: PluginManifest[] = [
credentialsPluginManifest,
topicPluginManifest,
hbarPluginManifest,
splitPaymentsPluginManifest,
subgraphPluginManifest,
saucerswapPluginManifest,
contractPluginManifest,
configPluginManifest,
contractErc20PluginManifest,
Expand Down
32 changes: 32 additions & 0 deletions src/core/utils/register-path-aliases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Registers @/ path alias at runtime so require('@/core/...') resolves when
* running the compiled CLI (dist/). Needed when path aliases in emitted JS
* were not rewritten by tsc-alias (e.g. partial build or different env).
*/
import * as path from 'path';

// When this file lives in dist/core/utils/, dist root is two levels up
const distRoot = path.join(__dirname, '..', '..');

const Mod = require('module') as NodeModule & {
_resolveFilename(
request: string,
parent: object,
isMain: boolean,
options?: object,
): string;
};
const origResolve = Mod._resolveFilename;

Mod._resolveFilename = function (
request: string,
parent: object,
isMain: boolean,
options?: object,
): string {
if (request.startsWith('@/')) {
const resolved = path.join(distRoot, request.slice(2));
return origResolve.call(this, resolved, parent, isMain, options);
}
return origResolve.apply(this, [request, parent, isMain, options] as never);
};
1 change: 1 addition & 0 deletions src/hiero-cli.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env node

import './core/utils/register-path-aliases';
import './core/utils/json-serialize';

import { program } from 'commander';
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/network/__tests__/unit/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe('network plugin - list command', () => {
expect(result.status).toBe(Status.Success);

expect(mockedCheckMirrorNodeHealth).toHaveBeenCalledWith(
'https://testnet.mirrornode.hedera.com/api/v1',
'https://testnet.hedera.validationcloud.io/v1/MtxToj_lTWpHFpYDHBw33G3M5Qs4rBJr9nK39V07dbQ/api/v1',
);
expect(mockedCheckRpcHealth).toHaveBeenCalledWith(
'https://testnet.hashio.io/api',
Expand Down
Loading
Loading