SingularityDocs

Documentation · Tutorial

Your first signed transaction

Build one signed transaction with two transfers. Encode it, decode it, then change an amount and watch signature verification fail.

You need: a terminal, Node.js 22, npm, and basic JavaScript. Installation uses the network; the exercise runs offline with temporary keys. No wallet, funds or node is required.

Your result: two instructions, 264 wire bytes, a valid signed round-trip, and rejection of the modified message. Nothing is submitted.

1. Install the pinned SDK

Tested with Node 22.22.2, npm 9.2.0, and @0xsingularity/web3.js@0.2.0. Keep this lesson pinned to that package; release differences are tracked separately. Official package metadata.

In a new directory:

mkdir singularity-first-transaction
cd singularity-first-transaction
npm init -y
npm install --ignore-scripts --no-audit --no-fund --save-exact @0xsingularity/web3.js@0.2.0

Keep the generated lockfile to preserve the resolved dependencies. The package supplies Solana transaction types plus Singularity amount and serialization helpers. SDK exports.

2. Create the two-instruction message

Save this as first-transaction.cjs:

const assert = require('node:assert/strict');
const sdk = require('@0xsingularity/web3.js');
const {
  Keypair, Transaction, SystemProgram, serializeUnchecked, singToLamports,
} = sdk;
const sender = Keypair.generate();
const recipientA = Keypair.generate().publicKey;
const recipientB = Keypair.generate().publicKey;
// Fixed dummy blockhash makes the example wholly offline. Never submit it.
const transaction = new Transaction({
  feePayer: sender.publicKey,
  recentBlockhash: '11111111111111111111111111111111',
}).add(
  SystemProgram.transfer({
    fromPubkey: sender.publicKey,
    toPubkey: recipientA,
    lamports: singToLamports(0.001),
  }),
  SystemProgram.transfer({
    fromPubkey: sender.publicKey,
    toPubkey: recipientB,
    lamports: singToLamports(0.002),
  }),
);
transaction.sign(sender);
assert.equal(transaction.verifySignatures(), true);
const bytes = serializeUnchecked(transaction);
const decoded = Transaction.from(bytes);
assert.equal(decoded.instructions.length, 2);
assert.equal(decoded.verifySignatures(), true);
// Change one byte in the first transfer amount, retaining the old signature.
const tampered = Transaction.from(bytes);
tampered.instructions[0].data[4] ^= 1;
assert.equal(tampered.verifySignatures(), false);
console.log(JSON.stringify({
  sdkVersion: require('@0xsingularity/web3.js/package.json').version,
  instructionCount: decoded.instructions.length,
  wireBytes: bytes.length,
  signedRoundTrip: true,
  modifiedAmountRejected: true,
  submitted: false,
}, null, 2));

3. Run it and check the result

From the same directory, run:

node first-transaction.cjs

The fresh-directory run on 8 September 2026 at 20:17 UTC exited 0 and produced:

{
  "sdkVersion": "0.2.0",
  "instructionCount": 2,
  "wireBytes": 264,
  "signedRoundTrip": true,
  "modifiedAmountRejected": true,
  "submitted": false
}

The keys stay in memory. The all-ones blockhash is an offline fixture: do not submit this transaction to a node.

If you seeDo this
Module not foundRun the install command in this directory, then retry.
An assertion failureCompare your saved file with the example.
A punycode deprecation warningCheck the assertions and JSON result. The tested run passed with this dependency warning.

4. Try authorizing the changed message

The original signature fails after the amount changes. What happens if the sender signs that changed message?

Immediately after assert.equal(tampered.verifySignatures(), false);, add:

tampered.sign(sender);
assert.equal(tampered.verifySignatures(), true);
console.log('Modified message authorized by a new signature');

Run the file again. The added line prints before the JSON report, and both assertions pass: the old signature failed; the new signature authorizes the changed message. This variation also passed in the tested environment above.

5. What you checked

The instructions encode 0.001 and 0.002 SING: 1,000,000 and 2,000,000 lamports. The sender signs the complete message, including both instructions and their account permissions.

CheckpointResult
Sign, encode and decodeBoth instructions retain a valid signature.
Change an amountThe original signature fails.
Sign the changed messageThe new signature passes.

serializeUnchecked is a wire serializer, not a signature verifier. It bypasses an upstream packet-size assertion and can serialize incomplete signatures; the explicit verifySignatures() assertions are therefore meaningful. Larger applications still face account-index, instruction-encoding, SDK packing and node transport limits. Serialization implementation, amount conversion.

This exercise checks client construction and authorization. Funded execution, fees, inclusion, cross-VM behavior and durability require a node or engine fixture. In execution, the outer transaction owns its program writes; Transaction lifecycle explains rollback and the fee/nonce effects that can remain after failure.

Continue from this result

Next needContinue here
Understand what happens after real submissionTransaction lifecycle, then RPC and SDK reference
Create a persistent wallet or inspect a saved signatureCLI and wallet setup, then Inspect a transaction
Understand shared execution and local rollbackSystem overview and cross-VM composition
Explore foreign evidence or financial executionHILSA and Bombil
Updated Sep 8, 2026View source

On this page