Skip to content

Commit cbe52e2

Browse files
committed
fix: address reviewer feedback from nickgs1337 and RomuloSiebra
- Fix SmartContract type from 9 to 63 (transaction.proto) - Fix SC sub-types: deploy=scType:1, invoke=scType:0 (were swapped) - Fix callValue format to map of token ID to amount ({KLV: amount}) - Fix Freeze type from 2 to 4 (FreezeContractType) - Fix TransactionBuildRequest to use flat contracts array - Add builder methods to KleverChainClient (buildTransfer, buildDeploy, buildInvoke, buildFreeze) to move chain logic out of server.ts - Extract fetchWithTimeout to reduce duplication in fetchJson/postJson - Add wasmPath option to deploy_sc (reads file server-side) - Add caller field to query_sc and VMQueryRequest - Add log() alias for console.error with comment explaining MCP stderr
1 parent af25cef commit cbe52e2

File tree

7 files changed

+447
-177
lines changed

7 files changed

+447
-177
lines changed

src/chain/client.test.ts

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,14 +407,162 @@ describe('KleverChainClient', () => {
407407
type: 0,
408408
sender: 'klv1sender',
409409
nonce: 5,
410-
contract: [{ type: 0, parameter: { amount: 1000000, toAddress: 'klv1receiver' } }],
410+
contracts: [{ amount: 1000000, toAddress: 'klv1receiver' }],
411411
});
412412

413413
expect(result.result.txHash).toBe('hash123');
414414
expect(result.result.tx).toBe('proto_encoded_tx');
415415
});
416416
});
417417

418+
describe('querySmartContract with caller', () => {
419+
it('passes caller field in VM query', async () => {
420+
const vmResult = {
421+
returnData: ['AQID'],
422+
returnCode: 'ok',
423+
returnMessage: '',
424+
};
425+
426+
mockFetch.mockResolvedValueOnce(
427+
jsonResponse({ data: vmResult, error: '', code: 'successful' })
428+
);
429+
430+
await client.querySmartContract({
431+
scAddress: 'klv1contract',
432+
funcName: 'getBalance',
433+
caller: 'klv1caller',
434+
});
435+
436+
expect(mockFetch).toHaveBeenCalledWith(
437+
'https://node.testnet.klever.org/vm/query',
438+
expect.objectContaining({
439+
method: 'POST',
440+
body: JSON.stringify({
441+
scAddress: 'klv1contract',
442+
funcName: 'getBalance',
443+
caller: 'klv1caller',
444+
}),
445+
})
446+
);
447+
});
448+
});
449+
450+
describe('buildTransfer', () => {
451+
it('fetches nonce and builds a transfer transaction', async () => {
452+
// Mock getNonce
453+
mockFetch.mockResolvedValueOnce(
454+
jsonResponse({ data: { nonce: 10 }, error: '', code: 'successful' })
455+
);
456+
// Mock buildTransaction
457+
mockFetch.mockResolvedValueOnce(
458+
jsonResponse({
459+
data: { result: { txHash: 'hash1', tx: 'proto1' } },
460+
error: '',
461+
code: 'successful',
462+
})
463+
);
464+
465+
const result = await client.buildTransfer({
466+
sender: 'klv1sender',
467+
receiver: 'klv1receiver',
468+
amount: 5000000,
469+
});
470+
471+
expect(result.result.txHash).toBe('hash1');
472+
// Verify the buildTransaction POST body
473+
const postCall = mockFetch.mock.calls[1];
474+
const body = JSON.parse(postCall[1]?.body as string);
475+
expect(body.type).toBe(0);
476+
expect(body.contracts[0].amount).toBe(5000000);
477+
expect(body.contracts[0].toAddress).toBe('klv1receiver');
478+
});
479+
});
480+
481+
describe('buildDeploy', () => {
482+
it('builds a deploy transaction with correct type 63 and scType 1', async () => {
483+
mockFetch.mockResolvedValueOnce(
484+
jsonResponse({ data: { nonce: 3 }, error: '', code: 'successful' })
485+
);
486+
mockFetch.mockResolvedValueOnce(
487+
jsonResponse({
488+
data: { result: { txHash: 'deploy1', tx: 'deploy_proto' } },
489+
error: '',
490+
code: 'successful',
491+
})
492+
);
493+
494+
const result = await client.buildDeploy({
495+
sender: 'klv1deployer',
496+
wasmHex: 'deadbeef',
497+
});
498+
499+
expect(result.result.txHash).toBe('deploy1');
500+
const postCall = mockFetch.mock.calls[1];
501+
const body = JSON.parse(postCall[1]?.body as string);
502+
expect(body.type).toBe(63);
503+
expect(body.contracts[0].scType).toBe(1);
504+
expect(body.data).toEqual(['deadbeef']);
505+
});
506+
});
507+
508+
describe('buildInvoke', () => {
509+
it('builds an invoke transaction with correct type 63, scType 0, and callValue map', async () => {
510+
mockFetch.mockResolvedValueOnce(
511+
jsonResponse({ data: { nonce: 7 }, error: '', code: 'successful' })
512+
);
513+
mockFetch.mockResolvedValueOnce(
514+
jsonResponse({
515+
data: { result: { txHash: 'invoke1', tx: 'invoke_proto' } },
516+
error: '',
517+
code: 'successful',
518+
})
519+
);
520+
521+
const result = await client.buildInvoke({
522+
sender: 'klv1caller',
523+
scAddress: 'klv1contract',
524+
funcName: 'doSomething',
525+
args: ['AQID'],
526+
callValue: { KLV: 1000000 },
527+
});
528+
529+
expect(result.result.txHash).toBe('invoke1');
530+
const postCall = mockFetch.mock.calls[1];
531+
const body = JSON.parse(postCall[1]?.body as string);
532+
expect(body.type).toBe(63);
533+
expect(body.contracts[0].scType).toBe(0);
534+
expect(body.contracts[0].address).toBe('klv1contract');
535+
expect(body.contracts[0].callValue).toEqual({ KLV: 1000000 });
536+
expect(body.data).toEqual(['doSomething', 'AQID']);
537+
});
538+
});
539+
540+
describe('buildFreeze', () => {
541+
it('builds a freeze transaction with correct type 4', async () => {
542+
mockFetch.mockResolvedValueOnce(
543+
jsonResponse({ data: { nonce: 5 }, error: '', code: 'successful' })
544+
);
545+
mockFetch.mockResolvedValueOnce(
546+
jsonResponse({
547+
data: { result: { txHash: 'freeze1', tx: 'freeze_proto' } },
548+
error: '',
549+
code: 'successful',
550+
})
551+
);
552+
553+
const result = await client.buildFreeze({
554+
sender: 'klv1sender',
555+
amount: 10000000,
556+
});
557+
558+
expect(result.result.txHash).toBe('freeze1');
559+
const postCall = mockFetch.mock.calls[1];
560+
const body = JSON.parse(postCall[1]?.body as string);
561+
expect(body.type).toBe(4);
562+
expect(body.contracts[0].amount).toBe(10000000);
563+
});
564+
});
565+
418566
describe('getNodeStatus', () => {
419567
it('fetches node health status', async () => {
420568
const statusData = {

src/chain/client.ts

Lines changed: 109 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
* Uses native fetch (Node 18+) — no external HTTP dependencies.
66
*/
77

8+
import {
9+
ContractType,
10+
SCType,
11+
} from './types.js';
812
import type {
913
KleverNetwork,
1014
NetworkConfig,
@@ -21,6 +25,10 @@ import type {
2125
NodeStatusData,
2226
TransactionBuildRequest,
2327
TransactionBuildData,
28+
TransferParams,
29+
DeployParams,
30+
InvokeParams,
31+
FreezeParams,
2432
} from './types.js';
2533

2634
/** Network URL mapping */
@@ -86,22 +94,19 @@ export class KleverChainClient {
8694

8795
// ─── Core HTTP Methods ───────────────────────────────────
8896

89-
private async fetchJson<T>(url: string): Promise<T> {
97+
private async fetchWithTimeout(url: string, init?: RequestInit): Promise<Response> {
9098
const controller = new AbortController();
9199
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
92100

93101
try {
94-
const response = await fetch(url, {
95-
signal: controller.signal,
96-
headers: { Accept: 'application/json' },
97-
});
102+
const response = await fetch(url, { ...init, signal: controller.signal });
98103

99104
if (!response.ok) {
100105
const text = await response.text().catch(() => '');
101106
throw new Error(`HTTP ${response.status}: ${text || response.statusText}`);
102107
}
103108

104-
return (await response.json()) as T;
109+
return response;
105110
} catch (error) {
106111
if (error instanceof Error && error.name === 'AbortError') {
107112
throw new Error(`Request timed out after ${this.timeout}ms: ${url}`);
@@ -112,35 +117,23 @@ export class KleverChainClient {
112117
}
113118
}
114119

115-
private async postJson<T>(url: string, body: unknown): Promise<T> {
116-
const controller = new AbortController();
117-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
118-
119-
try {
120-
const response = await fetch(url, {
121-
method: 'POST',
122-
signal: controller.signal,
123-
headers: {
124-
'Content-Type': 'application/json',
125-
Accept: 'application/json',
126-
},
127-
body: JSON.stringify(body),
128-
});
129-
130-
if (!response.ok) {
131-
const text = await response.text().catch(() => '');
132-
throw new Error(`HTTP ${response.status}: ${text || response.statusText}`);
133-
}
120+
private async fetchJson<T>(url: string): Promise<T> {
121+
const response = await this.fetchWithTimeout(url, {
122+
headers: { Accept: 'application/json' },
123+
});
124+
return (await response.json()) as T;
125+
}
134126

135-
return (await response.json()) as T;
136-
} catch (error) {
137-
if (error instanceof Error && error.name === 'AbortError') {
138-
throw new Error(`Request timed out after ${this.timeout}ms: ${url}`);
139-
}
140-
throw error;
141-
} finally {
142-
clearTimeout(timeoutId);
143-
}
127+
private async postJson<T>(url: string, body: unknown): Promise<T> {
128+
const response = await this.fetchWithTimeout(url, {
129+
method: 'POST',
130+
headers: {
131+
'Content-Type': 'application/json',
132+
Accept: 'application/json',
133+
},
134+
body: JSON.stringify(body),
135+
});
136+
return (await response.json()) as T;
144137
}
145138

146139
/** Unwrap a Klever API response, throwing on error */
@@ -235,6 +228,88 @@ export class KleverChainClient {
235228
return this.unwrap(response, `querySmartContract(${request.scAddress}::${request.funcName})`);
236229
}
237230

231+
// ─── Transaction Builder Methods ────────────────────────
232+
233+
/** Build an unsigned transfer (KLV or KDA) transaction */
234+
async buildTransfer(
235+
params: TransferParams,
236+
network?: KleverNetwork
237+
): Promise<TransactionBuildData> {
238+
const nonce = await this.getNonce(params.sender, network);
239+
240+
const contracts: Array<Record<string, unknown>> = [
241+
{
242+
amount: params.amount,
243+
toAddress: params.receiver,
244+
...(params.assetId ? { assetId: params.assetId } : {}),
245+
},
246+
];
247+
248+
return this.buildTransaction(
249+
{ type: ContractType.Transfer, sender: params.sender, nonce, contracts },
250+
network
251+
);
252+
}
253+
254+
/** Build an unsigned smart contract deploy transaction */
255+
async buildDeploy(
256+
params: DeployParams,
257+
network?: KleverNetwork
258+
): Promise<TransactionBuildData> {
259+
const nonce = await this.getNonce(params.sender, network);
260+
261+
const data = [params.wasmHex, ...(params.initArgs || [])];
262+
263+
const contracts: Array<Record<string, unknown>> = [
264+
{ scType: SCType.SCDeploy },
265+
];
266+
267+
return this.buildTransaction(
268+
{ type: ContractType.SmartContract, sender: params.sender, nonce, contracts, data },
269+
network
270+
);
271+
}
272+
273+
/** Build an unsigned smart contract invoke transaction */
274+
async buildInvoke(
275+
params: InvokeParams,
276+
network?: KleverNetwork
277+
): Promise<TransactionBuildData> {
278+
const nonce = await this.getNonce(params.sender, network);
279+
280+
const data = [params.funcName, ...(params.args || [])];
281+
282+
const contracts: Array<Record<string, unknown>> = [
283+
{
284+
scType: SCType.SCInvoke,
285+
address: params.scAddress,
286+
...(params.callValue ? { callValue: params.callValue } : {}),
287+
},
288+
];
289+
290+
return this.buildTransaction(
291+
{ type: ContractType.SmartContract, sender: params.sender, nonce, contracts, data },
292+
network
293+
);
294+
}
295+
296+
/** Build an unsigned freeze KLV transaction */
297+
async buildFreeze(
298+
params: FreezeParams,
299+
network?: KleverNetwork
300+
): Promise<TransactionBuildData> {
301+
const nonce = await this.getNonce(params.sender, network);
302+
303+
const contracts: Array<Record<string, unknown>> = [
304+
{ amount: params.amount },
305+
];
306+
307+
return this.buildTransaction(
308+
{ type: ContractType.Freeze, sender: params.sender, nonce, contracts },
309+
network
310+
);
311+
}
312+
238313
// ─── Transaction Operations ──────────────────────────────
239314

240315
/** Get transaction details by hash (from node) */

src/chain/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
export { KleverChainClient, NETWORK_CONFIGS } from './client.js';
22
export type { ChainClientOptions } from './client.js';
3+
export {
4+
ContractType,
5+
SCType,
6+
} from './types.js';
37
export type {
48
KleverNetwork,
59
NetworkConfig,
@@ -16,4 +20,8 @@ export type {
1620
NodeStatusData,
1721
TransactionBuildRequest,
1822
TransactionBuildData,
23+
TransferParams,
24+
DeployParams,
25+
InvokeParams,
26+
FreezeParams,
1927
} from './types.js';

0 commit comments

Comments
 (0)