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.ts→NearMode,nearModeToString - Meson:
ts/src/nodes/meson/common.ts→MesonMode,MesonModeToString - Mayan:
ts/src/nodes/mayan/common.ts→MayanMode,mayanModeToString - GasZip:
ts/src/nodes/gaszip/common.ts→GasZipMode,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 setscapabilitiesandnodeConfig - NEAR:
ts/src/nodes/near/common.ts→ constructor seeds tokens, chains, capabilities, and asyncsyncState() - Mayan:
ts/src/nodes/mayan/common.ts→ initialsupportedTokens, chains, andcapabilities - GasZip:
ts/src/nodes/gaszip/common.ts→capabilitiesandnodeConfigfor 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.ts→syncState()loads/listand builds tokens/chains/route map - NEAR:
ts/src/nodes/near/common.ts→syncState()merges NEAR Intents tokens with raw mappings and native aliases - GasZip:
ts/src/nodes/gaszip/common.ts→syncState()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.ts→isSwapEnabled(),normalizeChainId(),findAssetId()and fallbacks - Meson:
ts/src/nodes/meson/common.ts→isSwapEnabled()accepts same-chain and symbol-based matches - GasZip:
ts/src/nodes/gaszip/common.ts→isSwapEnabled()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.ts→processRequest(),processNearIntentsRequest(),generateDepositCalldata() - Meson:
ts/src/nodes/meson/common.ts→processRequest(),getPriceQuote(),getTransactionData() - GasZip:
ts/src/nodes/gaszip/common.ts→processRequest(),convertToGasZipDecimals(), Solana calldata helper insolana-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.ts→getTransactionRecord() - GasZip:
ts/src/nodes/gaszip/common.ts→getTransactionRecord()andmapStatus()
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
- Required Interfaces - Understand the Node interface and data types
- Implementation Examples - See real-world examples
- Testing - Write tests for your node