Skip to main content

Step-by-Step Integration

This guide walks you through integrating a new node into the Xplore Nodes system.

Step 1: Create Node Directory Structure

Create a new directory for your node under ts/src/nodes/:

ts/src/nodes/your-node/
├── common.ts # Main node implementation
├── index.ts # Exports
├── flows/ # Different modes/flows
│ ├── index.ts
│ └── api.ts # Default flow
└── tests/ # Test files
└── your-node.test.ts

Step 2: Define Node Modes (Optional)

If your node supports multiple modes, define an enum:

// ts/src/nodes/your-node/common.ts
export enum YourNodeMode {
NATIVE,
ADVANCED,
// Add more modes as needed
}

export function yourNodeModeToString(mode: YourNodeMode): string {
switch (mode) {
case YourNodeMode.NATIVE:
return "your_node_native";
case YourNodeMode.ADVANCED:
return "your_node_advanced";
default:
return "unknown";
}
}

Reference Examples:

  • NEAR: ts/src/nodes/near/common.tsNearMode, nearModeToString
  • Meson: ts/src/nodes/meson/common.tsMesonMode, MesonModeToString
  • Mayan: ts/src/nodes/mayan/common.tsMayanMode, mayanModeToString
  • GasZip: ts/src/nodes/gaszip/common.tsGasZipMode, GasZipModeToString

Step 3: Implement the Node Class

Create your main node class implementing the Node interface:

// ts/src/nodes/your-node/common.ts
import type { Node } from "../../node_trait.js";
import { NodeError, type NodeResult } from "../../error.js";
import {
TransactionType,
type NodeCapabilities,
type NodeSpecificConfig,
} from "../../types.js";
import {
type ChainToken,
type SwapRequest,
type SwapResponse,
type TransactionRecord,
NodeType,
} from "../../xplore-types/index.js";

class YourNode implements Node {
mode: YourNodeMode;
id: string;
supportedTokens: ChainToken[] = [];
reserveTokens: ChainToken[] = [];
supportedChainIds: string[] = [];
routeMap: Set<ChainToken>;
capabilities: NodeCapabilities;
nodeConfig: NodeSpecificConfig;

constructor(mode: YourNodeMode) {
this.mode = mode;
this.id = yourNodeModeToString(mode);
this.routeMap = new Set();

// Initialize capabilities
this.capabilities = {
supportsSwaps: true,
supportsCrossChain: true,
supportsNativeWrapping: false,
maxSupportedChains: 50,
supportedTxTypes: [TransactionType.Evm],
};

// Initialize node configuration
this.nodeConfig = {
apiEndpoint: "https://api.your-service.com",
apiKey: process.env.YOUR_NODE_API_KEY || null,
headers: {
"Content-Type": "application/json",
},
timeout: 30000,
retryConfig: {
maxRetries: 3,
retryDelay: 1000,
backoffFactor: 2,
},
};
}

// Required methods - see Required Interfaces section for details
async processRequest(input: SwapRequest): Promise<NodeResult<SwapResponse>> { }
getSupportedTokens(): ChainToken[] { }
getReserveTokens(): ChainToken[] { }
getId(): string { }
async syncState(): Promise<NodeResult<void>> { }
isSwapEnabled(inputToken: ChainToken, outputToken: ChainToken): boolean { }
getSupportedChainIds(): string[] { }
async getTransactionRecord(transactionHash: string): Promise<NodeResult<TransactionRecord>> { }
}

Reference Examples:

  • Meson: ts/src/nodes/meson/common.ts → constructor sets capabilities and nodeConfig
  • NEAR: ts/src/nodes/near/common.ts → constructor seeds tokens, chains, capabilities, and async syncState()
  • Mayan: ts/src/nodes/mayan/common.ts → initial supportedTokens, chains, and capabilities
  • GasZip: ts/src/nodes/gaszip/common.tscapabilities and nodeConfig for native bridging

Step 4: Implement Required Methods

4.1 Basic Getters

getId(): string {
return this.id;
}

getSupportedTokens(): ChainToken[] {
return this.supportedTokens;
}

getReserveTokens(): ChainToken[] {
return this.reserveTokens;
}

getSupportedChainIds(): string[] {
return this.supportedChainIds;
}

4.2 State Synchronization

async syncState(): Promise<NodeResult<void>> {
try {
// Fetch supported chains
const chainsResponse = await fetch(`${this.nodeConfig.apiEndpoint}/chains`);
const chainsData = await chainsResponse.json();

// Update supported chain IDs
this.supportedChainIds = chainsData.chains.map((chain: any) => chain.id);

// Fetch supported tokens
const tokensResponse = await fetch(`${this.nodeConfig.apiEndpoint}/tokens`);
const tokensData = await tokensResponse.json();

// Convert to ChainToken format
this.supportedTokens = tokensData.tokens.map((token: any) => ({
chain_id: token.chainId,
address: token.address,
decimals: token.decimals,
}));

// Update route map
this.routeMap = new Set(this.supportedTokens);

console.log(`[${this.id}] Synced ${this.supportedTokens.length} tokens across ${this.supportedChainIds.length} chains`);

} catch (error) {
return new NodeError(
`Failed to sync state: ${error instanceof Error ? error.message : 'Unknown error'}`,
NodeError.errorCodes.NetworkError
);
}
}

Reference Examples:

  • Meson: ts/src/nodes/meson/common.tssyncState() loads /list and builds tokens/chains/route map
  • NEAR: ts/src/nodes/near/common.tssyncState() merges NEAR Intents tokens with raw mappings and native aliases
  • GasZip: ts/src/nodes/gaszip/common.tssyncState() consumes /chains, builds name↔ID maps and limits

4.3 Swap Validation

isSwapEnabled(inputToken: ChainToken, outputToken: ChainToken): boolean {
// Check if both tokens are supported
const inputSupported = this.supportedTokens.some(
token => token.chain_id === inputToken.chain_id &&
token.address.toLowerCase() === inputToken.address.toLowerCase()
);

const outputSupported = this.supportedTokens.some(
token => token.chain_id === outputToken.chain_id &&
token.address.toLowerCase() === outputToken.address.toLowerCase()
);

if (!inputSupported || !outputSupported) {
return false;
}

// Add any additional validation logic here
return true;
}

Reference Examples:

  • NEAR: ts/src/nodes/near/common.tsisSwapEnabled(), normalizeChainId(), findAssetId() and fallbacks
  • Meson: ts/src/nodes/meson/common.tsisSwapEnabled() accepts same-chain and symbol-based matches
  • GasZip: ts/src/nodes/gaszip/common.tsisSwapEnabled() ensures native→native and respects inbound/outbound capacity

4.4 Process Swap Request

async processRequest(input: SwapRequest): Promise<NodeResult<SwapResponse>> {
try {
// Validate input
if (!input.input_token || !input.output_token) {
return new NodeError(
"Missing input or output token",
NodeError.errorCodes.MissingInputToken
);
}

// Check if swap is enabled
if (!this.isSwapEnabled(input.input_token, input.output_token)) {
return new NodeError(
"Swap not supported for this token pair",
NodeError.errorCodes.SwapDisabled
);
}

// Make API call to get quote
const quoteResponse = await this.getQuote(input);
if (quoteResponse instanceof NodeError) {
return quoteResponse;
}

// Convert to SwapResponse format
const swapResponse: SwapResponse = {
uuid: quoteResponse.uuid,
amount_out: quoteResponse.amountOut,
output_token: input.output_token,
amount_in: input.amount_in,
amount_in_with_fee: quoteResponse.amountInWithFee || input.amount_in,
input_token: input.input_token,
refund_address: input.sender_address,
recipient_address: input.recipient_address,
chain_id: input.input_token.chain_id,
partner_id: 0,
deposit_address: quoteResponse.depositAddress,
quote_expiry: Date.now() + (input.timeout_ms || 300000),
execution_time: quoteResponse.executionTime || 300000,
slippage_tolerance: input.slippage_tolerance,
node_id: this.id,
node_type: NodeType.Bridge,
transaction_data: quoteResponse.transactionData || null,
};

return swapResponse;

} catch (error) {
return new NodeError(
`Failed to process request: ${error instanceof Error ? error.message : 'Unknown error'}`,
NodeError.errorCodes.Internal
);
}
}

Reference Examples:

  • NEAR: ts/src/nodes/near/common.tsprocessRequest(), processNearIntentsRequest(), generateDepositCalldata()
  • Meson: ts/src/nodes/meson/common.tsprocessRequest(), getPriceQuote(), getTransactionData()
  • GasZip: ts/src/nodes/gaszip/common.tsprocessRequest(), convertToGasZipDecimals(), Solana calldata helper in solana-calldata.ts

4.5 Transaction Record Retrieval

async getTransactionRecord(transactionHash: string): Promise<NodeResult<TransactionRecord>> {
try {
const response = await fetch(`${this.nodeConfig.apiEndpoint}/transaction/${transactionHash}`);

if (!response.ok) {
return new NodeError(
`Transaction not found: ${response.statusText}`,
NodeError.errorCodes.NotFound
);
}

const data = await response.json();

const record: TransactionRecord = {
source_transaction_hash: data.sourceTxHash,
destination_transaction_hash: data.destTxHash,
source_timestamp: data.sourceTimestamp,
destination_timestamp: data.destTimestamp,
node: this.id,
sender: data.sender,
amount_in: data.amountIn,
input_token: data.inputToken,
output_token: data.outputToken,
amount_out: data.amountOut,
recipient_address: data.recipient,
status: this.mapStatus(data.status),
};

return record;

} catch (error) {
return new NodeError(
`Failed to get transaction record: ${error instanceof Error ? error.message : 'Unknown error'}`,
NodeError.errorCodes.NetworkError
);
}
}

Reference Examples:

  • Mayan: ts/src/nodes/mayan/common.tsgetTransactionRecord()
  • GasZip: ts/src/nodes/gaszip/common.tsgetTransactionRecord() and mapStatus()

Step 5: Create Flow Registration

Create a flow file to register your node:

// ts/src/nodes/your-node/flows/api.ts
import type { Node } from "../../../node_trait.js";
import { registerNode, type NodeDescriptor } from "../../../state.js";
import { YourNode, YourNodeMode } from "../common.js";

class ApiFlow implements NodeDescriptor {
node(): Node {
return new YourNode(YourNodeMode.NATIVE);
}
}

registerNode(new ApiFlow());

Step 6: Export Your Node

// ts/src/nodes/your-node/index.ts
export * from "./common.js";
export * from "./flows/index.js";
// ts/src/nodes/your-node/flows/index.ts
export * from "./api.js";

Step 7: Register in Main Index

Add your node to the main nodes index:

// ts/src/nodes/index.ts
export * from "./mayan/index.js";
export * from "./gaszip/index.js";
export * from "./meson/index.js";
export * from "./near/index.js";
export * from "./your-node/index.js"; // Add this line

Next Steps