Skip to content

Commit cc8b88e

Browse files
committed
mina-signer app which is a testing tool for simple transaction from A to B. It is use to test post HF network state
1 parent c82ec83 commit cc8b88e

File tree

8 files changed

+594
-0
lines changed

8 files changed

+594
-0
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Test Signer CLI
2+
3+
Command-line helper for drafting, signing, and broadcasting Mina payments via the public GraphQL API. It wraps the `mina-signer` library so you can submit transactions without wiring up a full wallet or SDK.
4+
5+
## Getting Started
6+
- **Prerequisites:** Node.js 18+ (for native `fetch`) and npm.
7+
- **Install dependencies:** `npm install`
8+
- **Quick run:** `node test-signer.js <private_key> <recipient_address> [graphql_url] [nonce]`
9+
10+
The optional `graphql_url` flag lets you override the default target defined in `config.js`.
11+
12+
## Workflow
13+
1. `test-signer.js` parses CLI arguments and wires the supporting services.
14+
2. `payment-service.js` derives the sender public key, composes a payment payload, and signs it with `mina-signer`.
15+
3. `graphql-client.js` sends the signed payload to the Mina daemon and can check whether the transaction reached the pool.
16+
4. `utils.js` provides small helpers for GraphQL string construction and CLI validation.
17+
5. `config.js` centralises network defaults and usage messaging.
18+
19+
Check the console output for a transaction id; you can re-run the pool check or the `getPooledUserCommands` helper to confirm inclusion.
20+
Provide a `nonce` argument when you need to synchronise with on-chain account state manually.
21+
The CLI prints emoji-enhanced step logs and a summary table so you can spot successes and failures at a glance.
22+
GraphQL errors (including malformed responses) cause the CLI to exit with a non-zero status so they can be surfaced in scripts and CI.
23+
24+
## File Guide
25+
- `test-signer.js` – CLI entry point orchestrating validation, signing, submission, and pool verification.
26+
- `payment-service.js` – Thin wrapper around `mina-signer` with sensible defaults for MINA amounts and fees.
27+
- `graphql-client.js` – Minimal fetch-based GraphQL transport for sending payments and querying pooled commands.
28+
- `utils.js` – GraphQL stringification helpers plus basic CLI argument validation/parsing.
29+
- `config.js` – Configuration constants and usage text surfaced by the CLI.
30+
- `key/` – Sample key material for experimentation; do not use in production environments.
31+
32+
## Customisation Tips
33+
- Update `CONFIG.DEFAULT_GRAPHQL_URL` in `config.js` to point at your daemon or a hosted GraphQL endpoint.
34+
- Tweak `CONFIG.MINA_UNITS.DEFAULT_AMOUNT_MULTIPLIER` and `DEFAULT_FEE_MULTIPLIER` to adjust the default transaction values.
35+
- Extend `GraphQLClient` with additional queries (e.g. account state, balances) if you need richer diagnostics.
36+
37+
## Private key format
38+
39+
For clarity, private key is in output format of:
40+
41+
```
42+
mina advanced dump-keypair --privkey-path ...
43+
```
44+
45+
## Safety Notes
46+
- Treat private keys in plain text with care. Prefer environment variables or a secure secrets manager for real deployments.
47+
- The example keys under `key/` are for local testing only; they are publicly known and should never hold funds.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Centralized configuration for Mina payment signing.
3+
* Keeps network defaults and unit conversion helpers in one place so
4+
* the rest of the code can remain declarative.
5+
*/
6+
export const CONFIG = {
7+
NETWORK: 'testnet',
8+
DEFAULT_GRAPHQL_URL: 'http://localhost:3085/graphql',
9+
MINA_UNITS: {
10+
ONE_MINA: 1000000000,
11+
DEFAULT_AMOUNT_MULTIPLIER: 150,
12+
DEFAULT_FEE_MULTIPLIER: 1
13+
}
14+
};
15+
16+
/**
17+
* Human-friendly CLI usage text that `test-signer.js` displays when
18+
* the caller provides incomplete arguments.
19+
*/
20+
export const USAGE_INFO = {
21+
message: 'Usage: node test-signer.js <private_key> <recipient_address> [graphql_url] [nonce]',
22+
example: 'Example: node test-signer.js <private_key> <recipient_address> http://localhost:3085/graphql 3',
23+
defaultUrl: `Default GraphQL URL: ${CONFIG.DEFAULT_GRAPHQL_URL}`
24+
};
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { GraphQLUtils } from './utils.js';
2+
3+
/**
4+
* Minimal GraphQL transport layer responsible for broadcasting signed
5+
* payments to a Mina daemon and inspecting the transaction pool.
6+
*/
7+
export class GraphQLClient {
8+
constructor(url) {
9+
this.url = url;
10+
}
11+
12+
/**
13+
* Posts a signed payment mutation to the configured GraphQL endpoint.
14+
* Surfaces detailed errors while preserving the structured response
15+
* the caller uses to confirm transaction submission.
16+
*/
17+
async sendPayment(signedPayment) {
18+
const query = GraphQLUtils.createPaymentMutation(signedPayment);
19+
20+
console.log('\n🚀 Sending payment via GraphQL');
21+
console.log(`🌐 Endpoint: ${this.url}`);
22+
console.log('📝 Mutation payload:');
23+
console.log(query);
24+
25+
try {
26+
const response = await fetch(this.url, {
27+
method: 'POST',
28+
headers: { 'Content-Type': 'application/json' },
29+
body: JSON.stringify({ operationName: null, query, variables: {} }),
30+
});
31+
32+
return await this.handleResponse(response);
33+
} catch (error) {
34+
throw new Error(`Request error: ${error.message}`);
35+
}
36+
}
37+
38+
/**
39+
* Normalizes the GraphQL response shape by either returning JSON data
40+
* or throwing a rich error that upstream callers can surface.
41+
*/
42+
async handleResponse(response) {
43+
if (response.status === 200) {
44+
const rawBody = await response.text();
45+
46+
let json;
47+
try {
48+
json = JSON.parse(rawBody);
49+
} catch (parseError) {
50+
throw new Error(
51+
`Unexpected JSON payload: ${parseError.message}. Raw response: ${rawBody}`
52+
);
53+
}
54+
55+
if (json.errors?.length) {
56+
const combinedErrors = json.errors
57+
.map(error => error.message ?? JSON.stringify(error))
58+
.join(' | ');
59+
throw new Error(`GraphQL errors: ${combinedErrors}`);
60+
}
61+
62+
console.log('📦 GraphQL response payload:');
63+
console.dir(json, { depth: null });
64+
return json;
65+
} else {
66+
const text = await response.text();
67+
throw new Error(`GraphQL error (${response.status}): ${text}`);
68+
}
69+
}
70+
71+
/**
72+
* Queries the daemon's pooled commands and returns true when the given
73+
* transaction ID is currently staged for inclusion in a block.
74+
*/
75+
async checkTransactionInPool(transactionId) {
76+
const query = `
77+
query MyQuery {
78+
pooledUserCommands {
79+
id
80+
}
81+
}
82+
`;
83+
84+
try {
85+
const response = await fetch(this.url, {
86+
method: 'POST',
87+
headers: { 'Content-Type': 'application/json' },
88+
body: JSON.stringify({
89+
operationName: 'MyQuery',
90+
query,
91+
variables: {}
92+
}),
93+
});
94+
95+
const rawBody = await response.text();
96+
if (response.status !== 200) {
97+
throw new Error(`GraphQL error (${response.status}): ${rawBody}`);
98+
}
99+
100+
let json;
101+
try {
102+
json = JSON.parse(rawBody);
103+
} catch (parseError) {
104+
throw new Error(
105+
`Unexpected JSON payload when checking pool: ${parseError.message}. Raw response: ${rawBody}`
106+
);
107+
}
108+
109+
if (json.errors?.length) {
110+
const combinedErrors = json.errors
111+
.map(error => error.message ?? JSON.stringify(error))
112+
.join(' | ');
113+
throw new Error(`GraphQL errors while checking pool: ${combinedErrors}`);
114+
}
115+
116+
const pooledCommands = json.data?.pooledUserCommands || [];
117+
return pooledCommands.some(command => command.id === transactionId);
118+
} catch (error) {
119+
console.error('Error checking transaction in pool:', error.message);
120+
throw error;
121+
}
122+
}
123+
124+
/**
125+
* Convenience method that lists transaction IDs in the current pool.
126+
* Useful for manual debugging or exploratory scripts.
127+
*/
128+
async getPooledUserCommands() {
129+
const query = `
130+
query MyQuery {
131+
pooledUserCommands {
132+
id
133+
}
134+
}
135+
`;
136+
137+
try {
138+
const response = await fetch(this.url, {
139+
method: 'POST',
140+
headers: { 'Content-Type': 'application/json' },
141+
body: JSON.stringify({
142+
operationName: 'MyQuery',
143+
query,
144+
variables: {}
145+
}),
146+
});
147+
148+
const rawBody = await response.text();
149+
if (response.status !== 200) {
150+
throw new Error(`GraphQL error (${response.status}): ${rawBody}`);
151+
}
152+
153+
let json;
154+
try {
155+
json = JSON.parse(rawBody);
156+
} catch (parseError) {
157+
throw new Error(
158+
`Unexpected JSON payload when fetching pooled commands: ${parseError.message}. Raw response: ${rawBody}`
159+
);
160+
}
161+
162+
if (json.errors?.length) {
163+
const combinedErrors = json.errors
164+
.map(error => error.message ?? JSON.stringify(error))
165+
.join(' | ');
166+
throw new Error(`GraphQL errors while fetching pooled commands: ${combinedErrors}`);
167+
}
168+
169+
console.log('📦 Pooled commands response payload:');
170+
console.dir(json, { depth: null });
171+
return json.data?.pooledUserCommands || [];
172+
} catch (error) {
173+
console.error('Error fetching pooled commands:', error.message);
174+
throw error;
175+
}
176+
}
177+
}

scripts/tests/mina-signer/package-lock.json

Lines changed: 35 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "test-signer",
3+
"type": "module",
4+
"version": "1.0.0",
5+
"main": "test-signer.js",
6+
"scripts": {
7+
"start": "node test-signer.js",
8+
"test": "echo \"Error: no test specified\" && exit 1"
9+
},
10+
"author": "",
11+
"license": "ISC",
12+
"description": "",
13+
"dependencies": {
14+
"mina-signer": "^1.0.0"
15+
}
16+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import Client from 'mina-signer';
2+
import { CONFIG } from './config.js';
3+
4+
/**
5+
* Thin wrapper around `mina-signer` that derives keys, builds payment
6+
* payloads with sensible defaults, and signs them for submission.
7+
*/
8+
export class PaymentService {
9+
constructor(network = CONFIG.NETWORK) {
10+
this.client = new Client({ network });
11+
}
12+
13+
/**
14+
* Translates a private key into its corresponding public key, delegating
15+
* to the Mina signer library.
16+
*/
17+
derivePublicKey(privateKey) {
18+
return this.client.derivePublicKey(privateKey);
19+
}
20+
21+
/**
22+
* Drafts a payment with reasonable defaults for nonce, fee, and amount.
23+
* The caller can override the amount via an options object or by passing
24+
* a numeric multiplier (legacy behaviour), and can now set an explicit nonce.
25+
*/
26+
createPayment(fromPrivateKey, toAddress, options = {}) {
27+
const publicKey = this.derivePublicKey(fromPrivateKey);
28+
const { ONE_MINA, DEFAULT_FEE_MULTIPLIER, DEFAULT_AMOUNT_MULTIPLIER } = CONFIG.MINA_UNITS;
29+
30+
let amountMultiplier = DEFAULT_AMOUNT_MULTIPLIER;
31+
let nonce = 0;
32+
33+
if (typeof options === 'number') {
34+
amountMultiplier = options;
35+
} else if (typeof options === 'object' && options !== null) {
36+
if (options.amountMultiplier !== undefined) {
37+
amountMultiplier = options.amountMultiplier;
38+
}
39+
if (options.nonce !== undefined) {
40+
nonce = options.nonce;
41+
}
42+
}
43+
44+
return {
45+
from: publicKey,
46+
to: toAddress,
47+
amount: ONE_MINA * amountMultiplier,
48+
nonce,
49+
fee: ONE_MINA * DEFAULT_FEE_MULTIPLIER,
50+
};
51+
}
52+
53+
/**
54+
* Signs a Mina payment object using the provided private key so it can
55+
* be broadcast via GraphQL.
56+
*/
57+
signPayment(payment, privateKey) {
58+
return this.client.signPayment(payment, privateKey);
59+
}
60+
}

0 commit comments

Comments
 (0)