diff --git a/embedded/modules/call/call.js b/embedded/modules/call/call.js index db3a981..caa0508 100644 --- a/embedded/modules/call/call.js +++ b/embedded/modules/call/call.js @@ -61,8 +61,10 @@ function buildParametersFromABI(functionDef, paramValues) { if (value !== undefined) { parameters.push({ - ParameterFlag: paramDef.flag, - ParameterValue: formatParameterValue(paramDef.type, value), + Parameter: { + ParameterName: Buffer.from(paramDef.name).toString('hex').toUpperCase(), + ParameterValue: formatParameterValue(paramDef.type, value), + }, }); } } @@ -148,6 +150,7 @@ async function callContract(config) { contract_account, function_name, network_url, + rpc_url, wallet_seed, abi_path, parameters, @@ -268,9 +271,8 @@ async function callContract(config) { // Local nodes: use HTTP RPC to avoid WebSocket hangs under emulation await client.disconnect(); - const rpcUrl = network_url - .replace('ws://', 'http://').replace('wss://', 'https://') - .replace('localhost:6006', 'localhost:5005'); + const rpcUrl = rpc_url || network_url + .replace('ws://', 'http://').replace('wss://', 'https://'); const submitResult = await httpRPC(rpcUrl, 'submit', { tx_blob: signed.tx_blob }, 120000); diff --git a/embedded/modules/deploy/deploy.js b/embedded/modules/deploy/deploy.js index 2f7c8bf..ff2bd1a 100644 --- a/embedded/modules/deploy/deploy.js +++ b/embedded/modules/deploy/deploy.js @@ -151,6 +151,7 @@ async function deployContract(config) { wasm_path, abi_path, network_url, + rpc_url, wallet_seed, faucet_url, fee, @@ -390,9 +391,8 @@ async function deployContract(config) { // Local nodes: use HTTP RPC to avoid WebSocket hangs under emulation await client.disconnect(); - const rpcUrl = network_url - .replace('ws://', 'http://').replace('wss://', 'https://') - .replace('localhost:6006', 'localhost:5005'); + const rpcUrl = rpc_url || network_url + .replace('ws://', 'http://').replace('wss://', 'https://'); const submitResult = await httpRPC(rpcUrl, 'submit', { tx_blob: signed.tx_blob }, 120000); diff --git a/internal/cli/account.go b/internal/cli/account.go index 6d85518..7e8aa13 100644 --- a/internal/cli/account.go +++ b/internal/cli/account.go @@ -52,6 +52,7 @@ func runAccount(cmd *cobra.Command, args []string) error { if accountNetwork == "local" { networkCfg = config.NetworkConfig{ URL: "ws://localhost:6006", + RPCURL: "http://localhost:5005", NetworkID: 63456, } } else { @@ -59,7 +60,7 @@ func runAccount(cmd *cobra.Command, args []string) error { } } - client := chain.NewClient(networkCfg.URL) + client := chain.NewClient(networkCfg.URL, networkCfg.GetRPCURL()) ctx := cmd.Context() switch subcommand { diff --git a/internal/cli/call.go b/internal/cli/call.go index b00f3d9..8b59ffb 100644 --- a/internal/cli/call.go +++ b/internal/cli/call.go @@ -71,6 +71,7 @@ func runCall(cmd *cobra.Command, args []string) error { if callNetwork == "local" { networkCfg = config.NetworkConfig{ URL: "ws://localhost:6006", + RPCURL: "http://localhost:5005", NetworkID: 0, // Local network uses network ID 0 } } else { @@ -134,6 +135,7 @@ func runCall(cmd *cobra.Command, args []string) error { ContractAccount: contractAccount, FunctionName: functionName, NetworkURL: networkCfg.URL, + RPCURL: networkCfg.GetRPCURL(), NetworkID: networkCfg.NetworkID, WalletSeed: walletSeed, Algorithm: callAlgorithm, diff --git a/internal/cli/clawback.go b/internal/cli/clawback.go index 5097200..fc30b80 100644 --- a/internal/cli/clawback.go +++ b/internal/cli/clawback.go @@ -56,7 +56,7 @@ func runClawback(cmd *cobra.Command, args []string) error { networkCfg, ok := cfg.Networks[clawbackNetwork] if !ok { if clawbackNetwork == "local" { - networkCfg = config.NetworkConfig{URL: "ws://localhost:6006", NetworkID: 63456} + networkCfg = config.NetworkConfig{URL: "ws://localhost:6006", RPCURL: "http://localhost:5005", NetworkID: 63456} } else { return fmt.Errorf("network '%s' not found in config", clawbackNetwork) } diff --git a/internal/cli/console.go b/internal/cli/console.go index 5f2672c..e616aca 100644 --- a/internal/cli/console.go +++ b/internal/cli/console.go @@ -50,6 +50,7 @@ func runConsole(cmd *cobra.Command, args []string) error { if consoleNetwork == "local" { networkCfg = config.NetworkConfig{ URL: "ws://localhost:6006", + RPCURL: "http://localhost:5005", NetworkID: 63456, } } else { diff --git a/internal/cli/delete.go b/internal/cli/delete.go index 5ac09cf..8fa71ee 100644 --- a/internal/cli/delete.go +++ b/internal/cli/delete.go @@ -53,7 +53,7 @@ func runDelete(cmd *cobra.Command, args []string) error { networkCfg, ok := cfg.Networks[deleteNetwork] if !ok { if deleteNetwork == "local" { - networkCfg = config.NetworkConfig{URL: "ws://localhost:6006", NetworkID: 63456} + networkCfg = config.NetworkConfig{URL: "ws://localhost:6006", RPCURL: "http://localhost:5005", NetworkID: 63456} } else { return fmt.Errorf("network '%s' not found in config", deleteNetwork) } diff --git a/internal/cli/deploy.go b/internal/cli/deploy.go index 2a7353b..a7761ec 100644 --- a/internal/cli/deploy.go +++ b/internal/cli/deploy.go @@ -80,6 +80,7 @@ func runDeploy(cmd *cobra.Command, args []string) error { if deployNetwork == "local" { networkCfg = config.NetworkConfig{ URL: "ws://localhost:6006", + RPCURL: "http://localhost:5005", NetworkID: 63456, FaucetURL: "http://localhost:8080/faucet", } @@ -220,6 +221,7 @@ func runDeploy(cmd *cobra.Command, args []string) error { WasmPath: wasmPath, ABIPath: abiPath, NetworkURL: networkCfg.URL, + RPCURL: networkCfg.GetRPCURL(), NetworkID: networkCfg.NetworkID, WalletSeed: walletSeed, Algorithm: deployAlgorithm, diff --git a/internal/cli/events.go b/internal/cli/events.go index bea765e..9e89e93 100644 --- a/internal/cli/events.go +++ b/internal/cli/events.go @@ -55,6 +55,7 @@ func runEvents(cmd *cobra.Command, args []string) error { if eventsNetwork == "local" { networkCfg = config.NetworkConfig{ URL: "ws://localhost:6006", + RPCURL: "http://localhost:5005", NetworkID: 63456, } } else { @@ -62,7 +63,7 @@ func runEvents(cmd *cobra.Command, args []string) error { } } - client := chain.NewClient(networkCfg.URL) + client := chain.NewClient(networkCfg.URL, networkCfg.GetRPCURL()) ctx := cmd.Context() color.Cyan("Contract Events\n") diff --git a/internal/cli/faucet.go b/internal/cli/faucet.go index c6e0e27..b150dc9 100644 --- a/internal/cli/faucet.go +++ b/internal/cli/faucet.go @@ -55,6 +55,7 @@ func runFaucet(cmd *cobra.Command, args []string) error { if isLocal { networkCfg = config.NetworkConfig{ URL: "ws://localhost:6006", + RPCURL: "http://localhost:5005", NetworkID: 0, // Local network uses network ID 0 } } else { diff --git a/internal/cli/info.go b/internal/cli/info.go index 6d22d84..5a3203c 100644 --- a/internal/cli/info.go +++ b/internal/cli/info.go @@ -51,6 +51,7 @@ func runInfo(cmd *cobra.Command, args []string) error { if infoNetwork == "local" { networkCfg = config.NetworkConfig{ URL: "ws://localhost:6006", + RPCURL: "http://localhost:5005", NetworkID: 63456, } } else { @@ -58,7 +59,7 @@ func runInfo(cmd *cobra.Command, args []string) error { } } - client := chain.NewClient(networkCfg.URL) + client := chain.NewClient(networkCfg.URL, networkCfg.GetRPCURL()) ctx := cmd.Context() color.Cyan("Contract Info\n") diff --git a/internal/cli/init.go b/internal/cli/init.go index b38ce92..0d848b2 100644 --- a/internal/cli/init.go +++ b/internal/cli/init.go @@ -100,6 +100,7 @@ ledger_interval = 1000 [networks.local] url = "ws://localhost:6006" +rpc_url = "http://localhost:5005" network_id = 63456 faucet_url = "http://localhost:8080/faucet" diff --git a/internal/cli/ledger.go b/internal/cli/ledger.go index 69e6b98..c061d4b 100644 --- a/internal/cli/ledger.go +++ b/internal/cli/ledger.go @@ -61,6 +61,7 @@ func runLedger(cmd *cobra.Command, args []string) error { if ledgerNetwork == "local" { networkCfg = config.NetworkConfig{ URL: "ws://localhost:6006", + RPCURL: "http://localhost:5005", NetworkID: 63456, } } else { @@ -68,7 +69,7 @@ func runLedger(cmd *cobra.Command, args []string) error { } } - client := chain.NewClient(networkCfg.URL) + client := chain.NewClient(networkCfg.URL, networkCfg.GetRPCURL()) ctx := cmd.Context() ledgerIndex := "current" @@ -108,6 +109,7 @@ func runTx(cmd *cobra.Command, args []string) error { if txNetwork == "local" { networkCfg = config.NetworkConfig{ URL: "ws://localhost:6006", + RPCURL: "http://localhost:5005", NetworkID: 63456, } } else { @@ -115,7 +117,7 @@ func runTx(cmd *cobra.Command, args []string) error { } } - client := chain.NewClient(networkCfg.URL) + client := chain.NewClient(networkCfg.URL, networkCfg.GetRPCURL()) ctx := cmd.Context() color.Cyan("Transaction Details\n") diff --git a/internal/cli/modify.go b/internal/cli/modify.go index b97df70..525d796 100644 --- a/internal/cli/modify.go +++ b/internal/cli/modify.go @@ -70,7 +70,7 @@ func runModify(cmd *cobra.Command, args []string) error { networkCfg, ok := cfg.Networks[modifyNetwork] if !ok { if modifyNetwork == "local" { - networkCfg = config.NetworkConfig{URL: "ws://localhost:6006", NetworkID: 63456} + networkCfg = config.NetworkConfig{URL: "ws://localhost:6006", RPCURL: "http://localhost:5005", NetworkID: 63456} } else { return fmt.Errorf("network '%s' not found in config", modifyNetwork) } diff --git a/internal/cli/node.go b/internal/cli/node.go index 410f4c9..11ed1e2 100644 --- a/internal/cli/node.go +++ b/internal/cli/node.go @@ -81,11 +81,21 @@ func nodeStart(ctx context.Context, manager *network.Manager) error { // Convert ledger interval from milliseconds to duration ledgerInterval := time.Duration(cfg.LocalNode.LedgerInterval) * time.Millisecond + // Resolve local network URLs from config + localCfg, ok := cfg.Networks["local"] + if !ok { + localCfg = config.NetworkConfig{ + URL: "ws://localhost:6006", + RPCURL: DefaultLocalRPCURL, + } + } + rpcURL := localCfg.GetRPCURL() + opts := network.StartOptions{ DockerImage: cfg.LocalNode.DockerImage, ConfigDir: cfg.LocalNode.ConfigDir, LedgerInterval: ledgerInterval, - RPCURL: DefaultLocalRPCURL, + RPCURL: rpcURL, } if err := manager.Start(ctx, opts); err != nil { @@ -98,7 +108,7 @@ func nodeStart(ctx context.Context, manager *network.Manager) error { fmt.Println() // Start the ledger daemon in background - if err := startLedgerDaemon(cfg.LocalNode.LedgerInterval); err != nil { + if err := startLedgerDaemon(cfg.LocalNode.LedgerInterval, rpcURL); err != nil { color.Yellow("Warning: Failed to start ledger daemon: %v\n", err) color.Yellow("Ledgers will not advance automatically.\n") } else { @@ -107,8 +117,8 @@ func nodeStart(ctx context.Context, manager *network.Manager) error { fmt.Println() color.Cyan("Connection Details:\n") - fmt.Println(" WebSocket URL: ws://localhost:6006") - fmt.Println(" RPC URL: http://localhost:5005") + fmt.Printf(" WebSocket URL: %s\n", localCfg.URL) + fmt.Printf(" RPC URL: %s\n", rpcURL) fmt.Println() color.Cyan("Ledger Service:\n") fmt.Printf(" Auto-advance interval: %v\n", ledgerInterval) @@ -123,7 +133,7 @@ func nodeStart(ctx context.Context, manager *network.Manager) error { } // startLedgerDaemon spawns the ledger daemon as a background process -func startLedgerDaemon(intervalMs int) error { +func startLedgerDaemon(intervalMs int, rpcURL string) error { // Get the path to the current executable executable, err := os.Executable() if err != nil { @@ -143,7 +153,7 @@ func startLedgerDaemon(intervalMs int) error { // Start the daemon process cmd := exec.Command(executable, "_ledger-daemon", - "--rpc-url", DefaultLocalRPCURL, + "--rpc-url", rpcURL, "--interval", strconv.Itoa(intervalMs), ) cmd.Stdout = logFile @@ -250,11 +260,23 @@ func nodeStatus(ctx context.Context, manager *network.Manager) error { } if status.Running { + // Resolve local network URLs from config + cfg, _ := config.LoadFromWorkingDir() + localCfg := config.NetworkConfig{ + URL: "ws://localhost:6006", + RPCURL: DefaultLocalRPCURL, + } + if cfg != nil { + if lc, ok := cfg.Networks["local"]; ok { + localCfg = lc + } + } + color.Green("✓ Local node is running\n") fmt.Println() color.Cyan("Connection Details:\n") - fmt.Println(" WebSocket URL: ws://localhost:6006") - fmt.Println(" RPC URL: http://localhost:5005") + fmt.Printf(" WebSocket URL: %s\n", localCfg.URL) + fmt.Printf(" RPC URL: %s\n", localCfg.GetRPCURL()) fmt.Println() // Show ledger daemon status diff --git a/internal/cli/rpc.go b/internal/cli/rpc.go index 4700d29..51e3a4b 100644 --- a/internal/cli/rpc.go +++ b/internal/cli/rpc.go @@ -49,6 +49,7 @@ func runRPC(cmd *cobra.Command, args []string) error { if rpcNetwork == "local" { networkCfg = config.NetworkConfig{ URL: "ws://localhost:6006", + RPCURL: "http://localhost:5005", NetworkID: 63456, } } else { @@ -56,7 +57,7 @@ func runRPC(cmd *cobra.Command, args []string) error { } } - client := chain.NewClient(networkCfg.URL) + client := chain.NewClient(networkCfg.URL, networkCfg.GetRPCURL()) ctx := cmd.Context() var params []interface{} diff --git a/internal/cli/user_delete.go b/internal/cli/user_delete.go index f7b42bd..e8e1196 100644 --- a/internal/cli/user_delete.go +++ b/internal/cli/user_delete.go @@ -53,7 +53,7 @@ func runUserDelete(cmd *cobra.Command, args []string) error { networkCfg, ok := cfg.Networks[userDeleteNetwork] if !ok { if userDeleteNetwork == "local" { - networkCfg = config.NetworkConfig{URL: "ws://localhost:6006", NetworkID: 63456} + networkCfg = config.NetworkConfig{URL: "ws://localhost:6006", RPCURL: "http://localhost:5005", NetworkID: 63456} } else { return fmt.Errorf("network '%s' not found in config", userDeleteNetwork) } diff --git a/pkg/caller/caller.go b/pkg/caller/caller.go index c954291..cb731f4 100644 --- a/pkg/caller/caller.go +++ b/pkg/caller/caller.go @@ -40,6 +40,10 @@ func (c *Caller) Call(ctx context.Context, config CallConfig) (*CallResult, erro "verbose": c.verbose, } + if config.RPCURL != "" { + jsConfig["rpc_url"] = config.RPCURL + } + if config.ABIPath != "" { jsConfig["abi_path"] = config.ABIPath } diff --git a/pkg/caller/types.go b/pkg/caller/types.go index f38fc9f..aeb40fb 100644 --- a/pkg/caller/types.go +++ b/pkg/caller/types.go @@ -16,6 +16,7 @@ type CallConfig struct { ContractAccount string FunctionName string NetworkURL string + RPCURL string // HTTP RPC URL (optional, derived from NetworkURL if empty) NetworkID uint32 WalletSeed string Algorithm string diff --git a/pkg/chain/client.go b/pkg/chain/client.go index 5dabcb2..0dc8536 100644 --- a/pkg/chain/client.go +++ b/pkg/chain/client.go @@ -35,9 +35,13 @@ type RPCError struct { Message string `json:"message"` } -// NewClient creates a new XRPL RPC client from a WebSocket or HTTP URL -func NewClient(url string) *Client { +// NewClient creates a new XRPL RPC client from a WebSocket or HTTP URL. +// An optional rpcURL can be provided to override the derived HTTP URL. +func NewClient(url string, rpcURL ...string) *Client { httpURL := toHTTPURL(url) + if len(rpcURL) > 0 && rpcURL[0] != "" { + httpURL = rpcURL[0] + } return &Client{ httpURL: httpURL, httpClient: &http.Client{ @@ -121,12 +125,12 @@ func (c *Client) CallTyped(ctx context.Context, target interface{}, method strin return nil } -// toHTTPURL converts a WebSocket URL to an HTTP URL for JSON-RPC +// toHTTPURL converts a WebSocket URL to an HTTP URL for JSON-RPC. +// This is a fallback conversion (ws:// -> http://, wss:// -> https://). +// For local development where ports differ, use NetworkConfig.GetRPCURL() or pass rpcURL to NewClient. func toHTTPURL(rawURL string) string { if strings.HasPrefix(rawURL, "ws://") { rawURL = strings.Replace(rawURL, "ws://", "http://", 1) - // Local dev: WS port 6006 maps to JSON-RPC port 5005 - rawURL = strings.Replace(rawURL, "localhost:6006", "localhost:5005", 1) } else if strings.HasPrefix(rawURL, "wss://") { rawURL = strings.Replace(rawURL, "wss://", "https://", 1) } diff --git a/pkg/config/types.go b/pkg/config/types.go index 6874c23..8fad144 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -2,6 +2,7 @@ package config import ( "runtime" + "strings" "time" ) @@ -33,11 +34,29 @@ type BuildConfig struct { type NetworkConfig struct { URL string `toml:"url"` + RPCURL string `toml:"rpc_url,omitempty"` NetworkID uint32 `toml:"network_id"` FaucetURL string `toml:"faucet_url,omitempty"` Explorer string `toml:"explorer,omitempty"` } +// GetRPCURL returns the RPC URL for this network. +// If rpc_url is explicitly set, it is returned as-is. +// Otherwise, the WebSocket URL is converted to HTTP (ws:// -> http://, wss:// -> https://). +func (n NetworkConfig) GetRPCURL() string { + if n.RPCURL != "" { + return n.RPCURL + } + url := n.URL + if strings.HasPrefix(url, "ws://") { + return strings.Replace(url, "ws://", "http://", 1) + } + if strings.HasPrefix(url, "wss://") { + return strings.Replace(url, "wss://", "https://", 1) + } + return url +} + type ContractConfig struct { Source string `toml:"source"` ABI string `toml:"abi"` diff --git a/pkg/console/repl.go b/pkg/console/repl.go index 87eecce..0d8c5c9 100644 --- a/pkg/console/repl.go +++ b/pkg/console/repl.go @@ -29,7 +29,7 @@ type REPL struct { func NewREPL(cfg *config.Config, contractAccount string, walletSeed string, networkCfg config.NetworkConfig) *REPL { return &REPL{ cfg: cfg, - client: chain.NewClient(networkCfg.URL), + client: chain.NewClient(networkCfg.URL, networkCfg.GetRPCURL()), contractAccount: contractAccount, walletSeed: walletSeed, networkCfg: networkCfg, diff --git a/pkg/deployer/deployer.go b/pkg/deployer/deployer.go index 20c497f..32507c0 100644 --- a/pkg/deployer/deployer.go +++ b/pkg/deployer/deployer.go @@ -39,6 +39,10 @@ func (d *Deployer) Deploy(ctx context.Context, config DeploymentConfig) (*Deploy "verbose": d.verbose, } + if config.RPCURL != "" { + jsConfig["rpc_url"] = config.RPCURL + } + if config.WalletSeed != "" { jsConfig["wallet_seed"] = config.WalletSeed } diff --git a/pkg/deployer/types.go b/pkg/deployer/types.go index 6b568af..048da16 100644 --- a/pkg/deployer/types.go +++ b/pkg/deployer/types.go @@ -16,6 +16,7 @@ type DeploymentConfig struct { WasmPath string ABIPath string NetworkURL string + RPCURL string // HTTP RPC URL (optional, derived from NetworkURL if empty) NetworkID uint32 WalletSeed string Algorithm string diff --git a/pkg/script/runner.go b/pkg/script/runner.go index 58cb8a2..4bef09b 100644 --- a/pkg/script/runner.go +++ b/pkg/script/runner.go @@ -278,6 +278,7 @@ func (r *Runner) resolveNetwork(name string) config.NetworkConfig { if name == "local" { return config.NetworkConfig{ URL: "ws://localhost:6006", + RPCURL: "http://localhost:5005", NetworkID: 63456, } } diff --git a/pkg/tester/fuzzer.go b/pkg/tester/fuzzer.go index 9598947..7f064c7 100644 --- a/pkg/tester/fuzzer.go +++ b/pkg/tester/fuzzer.go @@ -57,6 +57,7 @@ func (f *Fuzzer) Run(ctx context.Context, contractAccount string, walletSeed str if networkName == "local" { networkCfg = config.NetworkConfig{ URL: "ws://localhost:6006", + RPCURL: "http://localhost:5005", NetworkID: 63456, } } else { diff --git a/pkg/tester/integration.go b/pkg/tester/integration.go index 5a2216b..20bf746 100644 --- a/pkg/tester/integration.go +++ b/pkg/tester/integration.go @@ -110,6 +110,7 @@ func (r *IntegrationRunner) runSuite(ctx context.Context, suite *IntegrationTest if networkName == "local" { networkCfg = config.NetworkConfig{ URL: "ws://localhost:6006", + RPCURL: "http://localhost:5005", NetworkID: 63456, } } else { diff --git a/pkg/tester/invariant.go b/pkg/tester/invariant.go index 42ba9d0..0a171cd 100644 --- a/pkg/tester/invariant.go +++ b/pkg/tester/invariant.go @@ -57,6 +57,7 @@ func (r *InvariantRunner) Run(ctx context.Context, contractAccount string, walle if networkName == "local" { networkCfg = config.NetworkConfig{ URL: "ws://localhost:6006", + RPCURL: "http://localhost:5005", NetworkID: 63456, } } else {