@chainreactionom/nano-mcp
Version:
NANO cryptocurrency wallet implementation for MCP with comprehensive testing
616 lines (597 loc) • 15.7 kB
text/typescript
import {
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow';
import { checkAddress, checkHash, convert, Unit } from 'nanocurrency';
export class Nano implements INodeType {
description: INodeTypeDescription = {
displayName: 'Nano',
name: 'nano',
icon: 'file:nano.svg',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Interact with Nano (XNO) cryptocurrency via RPC',
defaults: {
name: 'Nano',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'nanoApi',
required: false,
displayOptions: {
show: {
authentication: ['credentials'],
},
},
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Public Node (SomeNano)',
value: 'public',
description: 'Use the public SomeNano RPC endpoint',
},
{
name: 'Custom Node',
value: 'credentials',
description: 'Use your own Nano node with credentials',
},
],
default: 'public',
description: 'How to authenticate with the Nano RPC',
},
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Account',
value: 'account',
},
{
name: 'Block',
value: 'block',
},
{
name: 'Network',
value: 'network',
},
{
name: 'Utility',
value: 'utility',
},
],
default: 'account',
},
// Account Operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['account'],
},
},
options: [
{
name: 'Get Balance',
value: 'getBalance',
description: 'Get account balance',
action: 'Get account balance',
},
{
name: 'Get Info',
value: 'getInfo',
description: 'Get account information',
action: 'Get account info',
},
{
name: 'Get History',
value: 'getHistory',
description: 'Get account transaction history',
action: 'Get account history',
},
{
name: 'Get Pending',
value: 'getPending',
description: 'Get pending transactions',
action: 'Get pending transactions',
},
{
name: 'Validate',
value: 'validate',
description: 'Validate account number',
action: 'Validate account',
},
],
default: 'getBalance',
},
// Block Operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['block'],
},
},
options: [
{
name: 'Get Info',
value: 'getBlockInfo',
description: 'Get block information',
action: 'Get block info',
},
{
name: 'Get Account',
value: 'getBlockAccount',
description: 'Get account that owns a block',
action: 'Get block account',
},
{
name: 'Get Count',
value: 'getBlockCount',
description: 'Get block count',
action: 'Get block count',
},
],
default: 'getBlockInfo',
},
// Network Operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['network'],
},
},
options: [
{
name: 'Get Representatives',
value: 'getRepresentatives',
description: 'Get list of representatives',
action: 'Get representatives',
},
{
name: 'Get Online Representatives',
value: 'getOnlineRepresentatives',
description: 'Get online representatives',
action: 'Get online representatives',
},
{
name: 'Get Difficulty',
value: 'getDifficulty',
description: 'Get current network difficulty',
action: 'Get network difficulty',
},
{
name: 'Get Supply',
value: 'getSupply',
description: 'Get available supply',
action: 'Get available supply',
},
{
name: 'Get Version',
value: 'getVersion',
description: 'Get node version',
action: 'Get node version',
},
],
default: 'getVersion',
},
// Utility Operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['utility'],
},
},
options: [
{
name: 'Convert Units',
value: 'convertUnits',
description: 'Convert between Nano units',
action: 'Convert units',
},
{
name: 'Get Price',
value: 'getPrice',
description: 'Get current Nano price',
action: 'Get Nano price',
},
{
name: 'Generate Key',
value: 'generateKey',
description: 'Generate a new private key',
action: 'Generate key',
},
],
default: 'convertUnits',
},
// Account fields
{
displayName: 'Account',
name: 'account',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['account'],
operation: ['getBalance', 'getInfo', 'getHistory', 'getPending', 'validate'],
},
},
placeholder: 'nano_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3',
description: 'The Nano account address',
},
// Block fields
{
displayName: 'Block Hash',
name: 'blockHash',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['block'],
operation: ['getBlockInfo', 'getBlockAccount'],
},
},
description: 'The block hash',
},
// History options
{
displayName: 'Count',
name: 'count',
type: 'number',
default: 10,
displayOptions: {
show: {
resource: ['account'],
operation: ['getHistory'],
},
},
description: 'Number of history entries to return',
},
// Pending options
{
displayName: 'Count',
name: 'pendingCount',
type: 'number',
default: 10,
displayOptions: {
show: {
resource: ['account'],
operation: ['getPending'],
},
},
description: 'Maximum number of pending blocks to return',
},
{
displayName: 'Threshold',
name: 'threshold',
type: 'string',
default: '0',
displayOptions: {
show: {
resource: ['account'],
operation: ['getPending'],
},
},
description: 'Minimum amount in raw for pending blocks',
},
// Unit conversion fields
{
displayName: 'Amount',
name: 'amount',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['utility'],
operation: ['convertUnits'],
},
},
description: 'Amount to convert',
},
{
displayName: 'From Unit',
name: 'fromUnit',
type: 'options',
options: [
{ name: 'Raw', value: 'raw' },
{ name: 'Nano', value: 'Nano' },
{ name: 'NANO', value: 'NANO' },
{ name: 'Mnano', value: 'Mnano' },
{ name: 'knano', value: 'knano' },
],
default: 'Nano',
displayOptions: {
show: {
resource: ['utility'],
operation: ['convertUnits'],
},
},
},
{
displayName: 'To Unit',
name: 'toUnit',
type: 'options',
options: [
{ name: 'Raw', value: 'raw' },
{ name: 'Nano', value: 'Nano' },
{ name: 'NANO', value: 'NANO' },
{ name: 'Mnano', value: 'Mnano' },
{ name: 'knano', value: 'knano' },
],
default: 'raw',
displayOptions: {
show: {
resource: ['utility'],
operation: ['convertUnits'],
},
},
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const authentication = this.getNodeParameter('authentication', 0) as string;
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
let rpcUrl = 'https://node.somenano.com/proxy';
if (authentication === 'credentials') {
const credentials = await this.getCredentials('nanoApi');
if (credentials.nodeUrl) {
rpcUrl = credentials.nodeUrl as string;
}
}
for (let i = 0; i < items.length; i++) {
try {
let responseData;
if (resource === 'account') {
if (operation === 'getBalance') {
const account = this.getNodeParameter('account', i) as string;
if (!checkAddress(account)) {
throw new NodeOperationError(this.getNode(), 'Invalid Nano account address');
}
const rpcData = await this.helpers.request({
method: 'POST',
uri: rpcUrl,
body: {
action: 'account_balance',
account,
},
json: true,
});
responseData = {
account,
balance: convert(rpcData.balance, { from: Unit.raw, to: Unit.Nano }),
pending: convert(rpcData.pending || rpcData.receivable || '0', { from: Unit.raw, to: Unit.Nano }),
balance_raw: rpcData.balance,
pending_raw: rpcData.pending || rpcData.receivable || '0',
};
} else if (operation === 'getInfo') {
const account = this.getNodeParameter('account', i) as string;
if (!checkAddress(account)) {
throw new NodeOperationError(this.getNode(), 'Invalid Nano account address');
}
responseData = await this.helpers.request({
method: 'POST',
uri: rpcUrl,
body: {
action: 'account_info',
account,
representative: true,
weight: true,
pending: true,
},
json: true,
});
} else if (operation === 'getHistory') {
const account = this.getNodeParameter('account', i) as string;
const count = this.getNodeParameter('count', i) as number;
if (!checkAddress(account)) {
throw new NodeOperationError(this.getNode(), 'Invalid Nano account address');
}
responseData = await this.helpers.request({
method: 'POST',
uri: rpcUrl,
body: {
action: 'account_history',
account,
count,
},
json: true,
});
} else if (operation === 'getPending') {
const account = this.getNodeParameter('account', i) as string;
const count = this.getNodeParameter('pendingCount', i) as number;
const threshold = this.getNodeParameter('threshold', i) as string;
if (!checkAddress(account)) {
throw new NodeOperationError(this.getNode(), 'Invalid Nano account address');
}
responseData = await this.helpers.request({
method: 'POST',
uri: rpcUrl,
body: {
action: 'pending',
account,
count,
threshold,
},
json: true,
});
} else if (operation === 'validate') {
const account = this.getNodeParameter('account', i) as string;
responseData = await this.helpers.request({
method: 'POST',
uri: rpcUrl,
body: {
action: 'validate_account_number',
account,
},
json: true,
});
}
} else if (resource === 'block') {
if (operation === 'getBlockInfo') {
const blockHash = this.getNodeParameter('blockHash', i) as string;
if (!checkHash(blockHash)) {
throw new NodeOperationError(this.getNode(), 'Invalid block hash');
}
responseData = await this.helpers.request({
method: 'POST',
uri: rpcUrl,
body: {
action: 'block_info',
hash: blockHash,
json_block: true,
},
json: true,
});
} else if (operation === 'getBlockAccount') {
const blockHash = this.getNodeParameter('blockHash', i) as string;
if (!checkHash(blockHash)) {
throw new NodeOperationError(this.getNode(), 'Invalid block hash');
}
responseData = await this.helpers.request({
method: 'POST',
uri: rpcUrl,
body: {
action: 'block_account',
hash: blockHash,
},
json: true,
});
} else if (operation === 'getBlockCount') {
responseData = await this.helpers.request({
method: 'POST',
uri: rpcUrl,
body: {
action: 'block_count',
},
json: true,
});
}
} else if (resource === 'network') {
if (operation === 'getRepresentatives') {
responseData = await this.helpers.request({
method: 'POST',
uri: rpcUrl,
body: {
action: 'representatives',
count: 100,
},
json: true,
});
} else if (operation === 'getOnlineRepresentatives') {
responseData = await this.helpers.request({
method: 'POST',
uri: rpcUrl,
body: {
action: 'representatives_online',
},
json: true,
});
} else if (operation === 'getDifficulty') {
responseData = await this.helpers.request({
method: 'POST',
uri: rpcUrl,
body: {
action: 'active_difficulty',
},
json: true,
});
} else if (operation === 'getSupply') {
responseData = await this.helpers.request({
method: 'POST',
uri: rpcUrl,
body: {
action: 'available_supply',
},
json: true,
});
} else if (operation === 'getVersion') {
responseData = await this.helpers.request({
method: 'POST',
uri: rpcUrl,
body: {
action: 'version',
},
json: true,
});
}
} else if (resource === 'utility') {
if (operation === 'convertUnits') {
const amount = this.getNodeParameter('amount', i) as string;
const fromUnit = this.getNodeParameter('fromUnit', i) as string;
const toUnit = this.getNodeParameter('toUnit', i) as string;
const converted = convert(amount, {
from: Unit[fromUnit as keyof typeof Unit],
to: Unit[toUnit as keyof typeof Unit],
});
responseData = {
input: amount,
fromUnit,
toUnit,
result: converted,
};
} else if (operation === 'getPrice') {
responseData = await this.helpers.request({
method: 'POST',
uri: rpcUrl,
body: {
action: 'price',
},
json: true,
});
} else if (operation === 'generateKey') {
responseData = await this.helpers.request({
method: 'POST',
uri: rpcUrl,
body: {
action: 'key_create',
},
json: true,
});
}
}
returnData.push({ json: responseData });
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ json: { error: error.message } });
continue;
}
throw error;
}
}
return [returnData];
}
}