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
649 changes: 27 additions & 622 deletions nodes/CrossmintWallets/CrossmintWallets.node.ts

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion shared/actions/checkout/purchaseProduct.operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { API_VERSIONS } from '../../utils/constants';
import { validateRequiredField } from '../../utils/validation';
import { signMessage } from '../../utils/blockchain';
import { TransactionCreateRequest, ApprovalRequest, ApiResponse } from '../../transport/types';
import { ChainFactory } from '../../chains/ChainFactory';

export async function purchaseProduct(
context: IExecuteFunctions,
Expand Down Expand Up @@ -55,7 +56,7 @@ export async function purchaseProduct(
signature,
messageToSign,
signerAddress,
chainType: 'solana',
chainType: ChainFactory.getChainTypeFromNetwork(chain) || 'solana',
chain
};

Expand Down
8 changes: 3 additions & 5 deletions shared/actions/wallet/getBalance.operation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IExecuteFunctions, NodeApiError } from 'n8n-workflow';
import { CrossmintApi } from '../../transport/CrossmintApi';
import { API_VERSIONS } from '../../utils/constants';
import { WalletApi } from '../../api/wallet.api';
import { buildWalletLocator } from '../../utils/locators';
import { WalletLocatorData, ApiResponse } from '../../transport/types';

Expand All @@ -15,13 +15,11 @@ export async function getBalance(
const chainType = context.getNodeParameter('balanceWalletChainType', itemIndex) as string;

const walletLocator = buildWalletLocator(walletResource, chainType, context, itemIndex);

const endpoint = `wallets/${walletLocator}/balances?chains=${encodeURIComponent(chains)}&tokens=${encodeURIComponent(tkn)}`;
const walletApi = new WalletApi(api);

try {
return await api.get(endpoint, API_VERSIONS.WALLETS);
return await walletApi.getBalance(walletLocator, chains, tkn);
} catch (error: unknown) {
// Pass through the original Crossmint API error exactly as received
throw new NodeApiError(context.getNode(), error as object & { message?: string });
}
}
3 changes: 2 additions & 1 deletion shared/actions/wallet/signTransaction.operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { API_VERSIONS, PAGINATION } from '../../utils/constants';
import { validatePrivateKey, validateRequiredField } from '../../utils/validation';
import { signMessage } from '../../utils/blockchain';
import { ApprovalRequest, ApiResponse } from '../../transport/types';
import { ChainFactory } from '../../chains/ChainFactory';

async function getTransactionStatus(
api: CrossmintApi,
Expand Down Expand Up @@ -127,7 +128,7 @@ export async function signTransaction(
signingDetails: {
signature: signature,
signedTransaction: signature,
chainType: 'solana',
chainType: ChainFactory.getChainTypeFromNetwork(chain) || rawResponseData.chainType || 'solana',
chain: chain,
transactionData: transactionData,
},
Expand Down
16 changes: 16 additions & 0 deletions shared/api/wallet.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { CrossmintApi } from '../transport/CrossmintApi';
import { API_VERSIONS } from '../utils/constants';
import { ApiResponse } from '../transport/types';

export class WalletApi {
constructor(private api: CrossmintApi) {}

async getBalance(
walletLocator: string,
chains: string,
tkn: string,
): Promise<ApiResponse> {
const endpoint = `wallets/${walletLocator}/balances?chains=${encodeURIComponent(chains)}&tokens=${encodeURIComponent(tkn)}`;
return await this.api.get(endpoint, API_VERSIONS.WALLETS);
}
}
126 changes: 126 additions & 0 deletions shared/chains/ChainFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { IChainProvider } from './IChainProvider';
import { SolanaProvider } from './solana/SolanaProvider';

/**
* Chain Factory
*
* Factory for creating blockchain provider instances.
* This is where new blockchain support is added.
*
* To add a new blockchain:
* 1. Create a new provider class implementing IChainProvider
* 2. Add a case in createProvider() method
* 3. That's it! The new chain will automatically appear in all node properties
*/
export class ChainFactory {
/**
* Creates a chain provider instance for the specified blockchain type
*
* @param chainType - The blockchain type (e.g., 'solana', 'evm', 'bitcoin')
* @returns The chain provider instance or undefined if not supported
*/
static createProvider(chainType: string): IChainProvider | undefined {
switch (chainType.toLowerCase()) {
case 'solana':
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using CHAIN_TYPES.SOLANA constant from constants.ts instead of string literal for consistency with custom rule 0e60096d

Context Used: Rule from dashboard - Use enum constants (e.g., Chain.SOLANA, Chain.BASE) instead of string literals when comparing chain ... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: shared/chains/ChainFactory.ts
Line: 24:24

Comment:
Consider using `CHAIN_TYPES.SOLANA` constant from `constants.ts` instead of string literal for consistency with custom rule 0e60096d

**Context Used:** Rule from `dashboard` - Use enum constants (e.g., Chain.SOLANA, Chain.BASE) instead of string literals when comparing chain ... ([source](https://app.greptile.com/review/custom-context?memory=0e60096d-0843-4800-801b-f8a78b766fbc))

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use CHAIN_TYPES.SOLANA constant from constants.ts instead of string literal for consistency and type safety.

Suggested change
case 'solana':
case CHAIN_TYPES.SOLANA:

Context Used: Rule from dashboard - Use enum constants (e.g., Chain.SOLANA, Chain.BASE) instead of string literals when comparing chain ... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: shared/chains/ChainFactory.ts
Line: 24:24

Comment:
Use `CHAIN_TYPES.SOLANA` constant from `constants.ts` instead of string literal for consistency and type safety.

```suggestion
			case CHAIN_TYPES.SOLANA:
```

**Context Used:** Rule from `dashboard` - Use enum constants (e.g., Chain.SOLANA, Chain.BASE) instead of string literals when comparing chain ... ([source](https://app.greptile.com/review/custom-context?memory=0e60096d-0843-4800-801b-f8a78b766fbc))

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

return new SolanaProvider();
// Future chains can be added here:
// case 'evm':
// return new EVMProvider();
// case 'bitcoin':
// return new BitcoinProvider();
default:
return undefined;
}
}

/**
* Get all supported chain types
*
* @returns Array of supported chain type identifiers
*/
static getSupportedChainTypes(): string[] {
return ['solana'];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using CHAIN_TYPES.SOLANA constant from constants.ts instead of string literal

Context Used: Rule from dashboard - Use enum constants (e.g., Chain.SOLANA, Chain.BASE) instead of string literals when comparing chain ... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: shared/chains/ChainFactory.ts
Line: 42:42

Comment:
Consider using `CHAIN_TYPES.SOLANA` constant from `constants.ts` instead of string literal

**Context Used:** Rule from `dashboard` - Use enum constants (e.g., Chain.SOLANA, Chain.BASE) instead of string literals when comparing chain ... ([source](https://app.greptile.com/review/custom-context?memory=0e60096d-0843-4800-801b-f8a78b766fbc))

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +41 to +42
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use CHAIN_TYPES.SOLANA constant instead of string literal.

Suggested change
static getSupportedChainTypes(): string[] {
return ['solana'];
return [CHAIN_TYPES.SOLANA];

Context Used: Rule from dashboard - Use enum constants (e.g., Chain.SOLANA, Chain.BASE) instead of string literals when comparing chain ... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: shared/chains/ChainFactory.ts
Line: 41:42

Comment:
Use `CHAIN_TYPES.SOLANA` constant instead of string literal.

```suggestion
		return [CHAIN_TYPES.SOLANA];
```

**Context Used:** Rule from `dashboard` - Use enum constants (e.g., Chain.SOLANA, Chain.BASE) instead of string literals when comparing chain ... ([source](https://app.greptile.com/review/custom-context?memory=0e60096d-0843-4800-801b-f8a78b766fbc))

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

// When adding new chains, add them here:
// return ['solana', 'evm', 'bitcoin'];
}

/**
* Get all available chain providers
*
* @returns Array of all chain provider instances
*/
static getAllProviders(): IChainProvider[] {
return this.getSupportedChainTypes()
.map(chainType => this.createProvider(chainType))
.filter((provider): provider is IChainProvider => provider !== undefined);
}

/**
* Check if a chain type is supported
*
* @param chainType - The chain type to check
* @returns True if the chain is supported
*/
static isSupported(chainType: string): boolean {
return this.getSupportedChainTypes().includes(chainType.toLowerCase());
}

/**
* Get chain options for n8n node properties
*
* @returns Array of options suitable for n8n node properties
*/
static getChainOptions(): Array<{ name: string; value: string; description: string }> {
return this.getAllProviders().map(provider => ({
name: provider.displayName,
value: provider.chainType,
description: `${provider.displayName} blockchain`,
}));
}

/**
* Get network options for a specific chain
*
* @param chainType - The chain type
* @returns Array of network options or empty array if chain not found
*/
static getNetworkOptions(chainType: string): Array<{ name: string; value: string; description: string }> {
const provider = this.createProvider(chainType);
if (!provider) {
return [];
}

return provider.getNetworks().map(network => ({
name: network.name,
value: network.id,
description: network.isTestnet ? 'Testnet' : 'Mainnet',
}));
}

/**
* Get the chainType from a network identifier
* e.g., "solana-devnet" -> "solana", "ethereum" -> "evm", "polygon" -> "evm"
*
* @param network - The network identifier (e.g., "solana", "solana-devnet", "ethereum")
* @returns The chainType or undefined if not found
*/
static getChainTypeFromNetwork(network: string): string | undefined {
const normalizedNetwork = network.toLowerCase();

for (const provider of this.getAllProviders()) {
const networks = provider.getNetworks();
if (networks.some(n => n.id.toLowerCase() === normalizedNetwork)) {
return provider.chainType;
}
}

// Fallback: check if network starts with a known chainType
for (const chainType of this.getSupportedChainTypes()) {
if (normalizedNetwork.startsWith(chainType)) {
return chainType;
}
}

return undefined;
}
}
167 changes: 167 additions & 0 deletions shared/chains/IChainProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* Blockchain Provider Interface
*
* This interface defines the contract for all blockchain implementations.
* Each blockchain (Solana, EVM, Bitcoin, etc.) must implement this interface
* to provide chain-specific functionality.
*/

/**
* Network configuration for a blockchain
*/
export interface ChainNetwork {
/** Network identifier (e.g., 'solana', 'solana-devnet', 'ethereum', 'base') */
id: string;
/** Display name for the network */
name: string;
/** Whether this is a testnet/devnet */
isTestnet: boolean;
/** Optional RPC endpoint */
rpcEndpoint?: string;
}

/**
* Transaction data structure for signing
*/
export interface TransactionData {
/** The message/hash to sign */
message: string;
/** Optional transaction ID from the API */
transactionId?: string;
/** Additional chain-specific data */
metadata?: Record<string, unknown>;
}

/**
* Signature result from transaction signing
*/
export interface SignatureResult {
/** The signature as a string */
signature: string;
/** The signer's address */
signerAddress: string;
/** Additional metadata about the signature */
metadata?: Record<string, unknown>;
}

/**
* Validation result
*/
export interface ValidationResult {
valid: boolean;
error?: string;
}

/**
* Chain Provider Interface
*
* Implement this interface to add support for a new blockchain.
*/
export interface IChainProvider {
/**
* Unique identifier for this chain type (e.g., 'solana', 'evm', 'bitcoin')
*/
readonly chainType: string;

/**
* Display name for the chain (e.g., 'Solana', 'Ethereum Virtual Machine')
*/
readonly displayName: string;

/**
* Get all available networks for this chain
* @returns Array of available networks
*/
getNetworks(): ChainNetwork[];

/**
* Get a specific network by its ID
* @param networkId - The network identifier
* @returns Network configuration or undefined if not found
*/
getNetwork(networkId: string): ChainNetwork | undefined;

/**
* Validate a wallet address for this chain
* @param address - The wallet address to validate
* @returns Validation result with optional error message
*/
validateAddress(address: string): ValidationResult;

/**
* Validate a private key for this chain
* @param privateKey - The private key to validate
* @returns Validation result with optional error message
*/
validatePrivateKey(privateKey: string): ValidationResult;

/**
* Format an address according to chain conventions
* (e.g., lowercase for EVM, as-is for Solana)
* @param address - The address to format
* @returns Formatted address
*/
formatAddress(address: string): string;

/**
* Derive the public address from a private key
* @param privateKey - The private key
* @returns The public address
* @throws Error if private key is invalid
*/
getAddressFromPrivateKey(privateKey: string): string;

/**
* Sign a transaction with a private key
* @param data - Transaction data to sign
* @param privateKey - The private key to sign with
* @returns Signature result
* @throws Error if signing fails
*/
signTransaction(data: TransactionData, privateKey: string): Promise<SignatureResult>;

/**
* Get the regex pattern for address validation
* Used for UI validation in n8n node properties
* @returns Regex pattern as string
*/
getAddressValidationRegex(): string;

/**
* Get the validation error message for addresses
* @returns Error message shown to users
*/
getAddressValidationError(): string;

/**
* Get example address for placeholder text
* @returns Example address string
*/
getExampleAddress(): string;
}

/**
* Base class for chain providers with common functionality
*/
export abstract class BaseChainProvider implements IChainProvider {
abstract readonly chainType: string;
abstract readonly displayName: string;

abstract getNetworks(): ChainNetwork[];
abstract getNetwork(networkId: string): ChainNetwork | undefined;
abstract validateAddress(address: string): ValidationResult;
abstract validatePrivateKey(privateKey: string): ValidationResult;
abstract formatAddress(address: string): string;
abstract getAddressFromPrivateKey(privateKey: string): string;
abstract signTransaction(data: TransactionData, privateKey: string): Promise<SignatureResult>;
abstract getAddressValidationRegex(): string;
abstract getAddressValidationError(): string;
abstract getExampleAddress(): string;

/**
* Helper method to create a validation result
*/
protected createValidationResult(valid: boolean, error?: string): ValidationResult {
return { valid, error };
}
}
13 changes: 13 additions & 0 deletions shared/chains/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Blockchain Abstraction Layer
*
* Central exports for all blockchain-related functionality.
*/

export * from './IChainProvider';
export * from './ChainFactory';
export * from './solana/SolanaProvider';

// Future exports:
// export * from './evm/EVMProvider';
// export * from './bitcoin/BitcoinProvider';
Loading