-
Notifications
You must be signed in to change notification settings - Fork 166
Simple mina-signer integration test app #2646
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # Test Signer CLI | ||
|
|
||
| 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. | ||
|
|
||
| ## Getting Started | ||
|
|
||
| - **Prerequisites:** Node.js 18+ (for native `fetch`) and npm. | ||
| - **Install dependencies:** `npm install` | ||
| - **Quick run:** | ||
| `node mina-test-signer.js <private_key> <recipient_address> [graphql_url] [nonce]` | ||
|
|
||
| The optional `graphql_url` flag lets you override the default target defined in | ||
| `config.js`. | ||
|
|
||
| ## Workflow | ||
|
|
||
| 1. `mina-test-signer.js` parses CLI arguments and wires the supporting services. | ||
| 2. `graphql-client.js` sends the signed payload to the Mina daemon and can check | ||
| whether the transaction reached the pool. | ||
| 3. `utils.js` provides small helpers for GraphQL string construction and CLI | ||
| validation. | ||
| 4. `config.js` centralises network defaults and usage messaging. | ||
|
|
||
| - `mina-test-signer.js` – CLI entry point orchestrating validation, signing, | ||
| submission, and pool verification. | ||
| - `payment-service.js` – Thin wrapper around `mina-signer` with sensible | ||
| defaults for MINA amounts and fees. | ||
| - `graphql-client.js` – Minimal fetch-based GraphQL transport for sending | ||
| payments and querying pooled commands. | ||
| - `utils.js` – GraphQL stringification helpers plus basic CLI argument | ||
| validation/parsing. | ||
| - `config.js` – Configuration constants and usage text surfaced by the CLI. | ||
| - `key/` – Sample key material for experimentation; do not use in production | ||
| environments. | ||
|
|
||
| Check the console output for a transaction id; you can re-run the pool check or | ||
| the `getPooledUserCommands` helper to confirm inclusion. Provide a `nonce` | ||
| argument when you need to synchronise with on-chain account state manually. The | ||
| CLI prints emoji-enhanced step logs and a summary table so you can spot | ||
| successes and failures at a glance. GraphQL errors (including malformed | ||
| responses) cause the CLI to exit with a non-zero status so they can be surfaced | ||
| in scripts and CI. | ||
|
|
||
| ## Private key format | ||
|
|
||
| For clarity, private key is in output format of: | ||
|
|
||
| ``` | ||
| mina advanced dump-keypair --privkey-path ... | ||
| ``` | ||
|
|
||
| ## Safety Notes | ||
|
|
||
| - Treat private keys in plain text with care. Prefer environment variables or a | ||
| secure secrets manager for real deployments. | ||
| - The example keys under `key/` are for local testing only; they are publicly | ||
| known and should never hold funds. | ||
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| /** | ||
| * Centralized configuration for Mina payment signing. | ||
| * Keeps network defaults and unit conversion helpers in one place so | ||
| * the rest of the code can remain declarative. | ||
| */ | ||
| export const CONFIG = { | ||
| NETWORK: 'testnet', | ||
| DEFAULT_GRAPHQL_URL: 'http://localhost:3085/graphql', | ||
| MINA_UNITS: { | ||
| ONE_MINA: 1000000000, | ||
| DEFAULT_AMOUNT_MULTIPLIER: 150, | ||
| DEFAULT_FEE_MULTIPLIER: 1 | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Human-friendly CLI usage text that `mina-test-signer.js` displays when | ||
| * the caller provides incomplete arguments. | ||
| */ | ||
| export const USAGE_INFO = { | ||
| message: 'Usage: node mina-test-signer.js <private_key> <recipient_address> [graphql_url] [nonce]', | ||
| example: 'Example: node mina-test-signer.js EKErBK1KznrJJY3raJafSyxSayJ6viejaVrmjzXkSmoxXiJQsesU B62qp4wcxoJyFFyXZ2RVw8kGPpWn6ncK4RtsTz29jFf6fY2XYN42R1v http://172.17.0.3:3085/graphql 3', | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i hope this private key is random x) |
||
| defaultUrl: `Default GraphQL URL: ${CONFIG.DEFAULT_GRAPHQL_URL}` | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| import { GraphQLUtils } from './utils.js'; | ||
|
|
||
| /** | ||
| * Minimal GraphQL transport layer responsible for broadcasting signed | ||
| * payments to a Mina daemon and inspecting the transaction pool. | ||
| */ | ||
| export class GraphQLClient { | ||
| constructor(url) { | ||
| this.url = url; | ||
| } | ||
|
|
||
| /** | ||
| * Posts a signed payment mutation to the configured GraphQL endpoint. | ||
| * Surfaces detailed errors while preserving the structured response | ||
| * the caller uses to confirm transaction submission. | ||
| */ | ||
| async sendPayment(signedPayment) { | ||
| const query = GraphQLUtils.createPaymentMutation(signedPayment); | ||
|
|
||
| console.log('\n🚀 Sending payment via GraphQL'); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i am personally not a big fan of heavy emoji use in our code bsae but i dont want to block this PR because of that |
||
| console.log(`🌐 Endpoint: ${this.url}`); | ||
| console.log('📝 Mutation payload:'); | ||
| console.log(query); | ||
|
|
||
| try { | ||
| const response = await fetch(this.url, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ query }), | ||
| }); | ||
|
|
||
| return await this.handleResponse(response); | ||
| } catch (error) { | ||
| throw new Error(`Request error: ${error.message}`); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Normalizes the GraphQL response shape by either returning JSON data | ||
| * or throwing a rich error that upstream callers can surface. | ||
| */ | ||
| async handleResponse(response) { | ||
| if (response.status === 200) { | ||
| const rawBody = await response.text(); | ||
|
|
||
| let json; | ||
| try { | ||
| json = JSON.parse(rawBody); | ||
| } catch (parseError) { | ||
| throw new Error( | ||
| `Unexpected JSON payload: ${parseError.message}. Raw response: ${rawBody}` | ||
| ); | ||
| } | ||
|
|
||
| if (json.errors?.length) { | ||
| const combinedErrors = json.errors | ||
| .map(error => error.message ?? JSON.stringify(error)) | ||
| .join(' | '); | ||
| throw new Error(`GraphQL errors: ${combinedErrors}`); | ||
| } | ||
|
|
||
| console.log('📦 GraphQL response payload:'); | ||
| console.dir(json, { depth: null }); | ||
| return json; | ||
| } else { | ||
| const text = await response.text(); | ||
| throw new Error(`GraphQL error (${response.status}): ${text}`); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Queries the daemon's pooled commands and returns true when the given | ||
| * transaction ID is currently staged for inclusion in a block. | ||
| */ | ||
| async checkTransactionInPool(transactionId) { | ||
| const query = ` | ||
| query MyQuery { | ||
| pooledUserCommands { | ||
| id | ||
| } | ||
| } | ||
| `; | ||
|
|
||
| try { | ||
| const response = await fetch(this.url, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ | ||
| operationName: 'MyQuery', | ||
| query, | ||
| variables: {} | ||
| }), | ||
| }); | ||
|
|
||
| const rawBody = await response.text(); | ||
| if (response.status !== 200) { | ||
| throw new Error(`GraphQL error (${response.status}): ${rawBody}`); | ||
| } | ||
|
|
||
| let json; | ||
| try { | ||
| json = JSON.parse(rawBody); | ||
| } catch (parseError) { | ||
| throw new Error( | ||
| `Unexpected JSON payload when checking pool: ${parseError.message}. Raw response: ${rawBody}` | ||
| ); | ||
| } | ||
|
|
||
| if (json.errors?.length) { | ||
| const combinedErrors = json.errors | ||
| .map(error => error.message ?? JSON.stringify(error)) | ||
| .join(' | '); | ||
| throw new Error(`GraphQL errors while checking pool: ${combinedErrors}`); | ||
| } | ||
|
|
||
| const pooledCommands = json.data?.pooledUserCommands || []; | ||
| return pooledCommands.some(command => command.id === transactionId); | ||
| } catch (error) { | ||
| console.error('Error checking transaction in pool:', error.message); | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Convenience method that lists transaction IDs in the current pool. | ||
| * Useful for manual debugging or exploratory scripts. | ||
| */ | ||
| async getPooledUserCommands() { | ||
| const query = ` | ||
| query MyQuery { | ||
| pooledUserCommands { | ||
| id | ||
| } | ||
| } | ||
| `; | ||
|
|
||
| try { | ||
| const response = await fetch(this.url, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ | ||
| operationName: 'MyQuery', | ||
| query, | ||
| variables: {} | ||
| }), | ||
| }); | ||
|
|
||
| const rawBody = await response.text(); | ||
| if (response.status !== 200) { | ||
| throw new Error(`GraphQL error (${response.status}): ${rawBody}`); | ||
| } | ||
|
|
||
| let json; | ||
| try { | ||
| json = JSON.parse(rawBody); | ||
| } catch (parseError) { | ||
| throw new Error( | ||
| `Unexpected JSON payload when fetching pooled commands: ${parseError.message}. Raw response: ${rawBody}` | ||
| ); | ||
| } | ||
|
|
||
| if (json.errors?.length) { | ||
| const combinedErrors = json.errors | ||
| .map(error => error.message ?? JSON.stringify(error)) | ||
| .join(' | '); | ||
| throw new Error(`GraphQL errors while fetching pooled commands: ${combinedErrors}`); | ||
| } | ||
|
|
||
| console.log('📦 Pooled commands response payload:'); | ||
| console.dir(json, { depth: null }); | ||
| return json.data?.pooledUserCommands || []; | ||
| } catch (error) { | ||
| console.error('Error fetching pooled commands:', error.message); | ||
| throw error; | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see
payment-service.jsorkey/There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This list also echoes the list above without a title or enhancing description, are we sure we need both lists?