A TypeScript monorepo for building on ngchain:
| Package | What |
|---|---|
@ngchain/sdk |
Core SDK — RPC client, keys/addresses, tx building & signing, the commit–reveal private-tx flow, and the injected-provider spec. Signing is validated byte-for-byte against a live node. |
@ngchain/wallet-extension |
A Manifest-V3 browser wallet built on the SDK: encrypted keyring, popup UI, and a window.ngchain dapp provider with per-request approval. |
@ngchain/react |
React connect kit — NgWalletProvider, hooks, and a ConnectButton (WalletConnect-style UX over the injected provider). |
npm install # install workspace deps
npm run build # build all packagesimport { NgClient, KeyPair, Network, ngToBaseUnits, baseUnitsToNg } from '@ngchain/sdk';
const ng = NgClient.http('http://testnet-rpc.ngchain.org:52521', Network.TESTNET); // or your node
const key = KeyPair.generate(); // 32-byte secp256k1; key.address is bs58
console.log(key.address);
const { total } = await ng.balance(key.address);
console.log(baseUnitsToNg(total), 'NG');
// a transfer is a one-call fire-and-forget: the SDK salts+signs the reveal,
// binds a commitment, and submits both via ng_sendPrivateTx (the node re-heights
// until they land). Fee is estimated from the node if omitted.
const txid = await ng.transfer(key, { to: recipient, value: ngToBaseUnits('1.5') });
// contracts
await ng.callContract(key, { contract, method: 'transfer', args, value: 0n });
await ng.deployContract(key, wasmBytes); // empty bytes = destroySigning details (matching ngtypes): address = base58(blake3(0x01 ‖ scheme ‖ pubkey));
effect txs sign the height-independent digest so one signature spans the reveal
window; the secp256k1 signature travels in the compact recover envelope
(0x03 ‖ scheme ‖ 65-byte sig).
A key is derived from a 32-byte seed exactly as ngcore's NewKeyFromSeed
(SHAKE256("ngcore-keygen" ‖ scheme ‖ seed)), so the wallet format scheme ‖ seed
is portable between this SDK and the node. Alongside secp256k1 (0x01), the SDK
signs natively with the post-quantum ML-DSA-44 (0x11, FIPS 204) and
SLH-DSA-128s (0x12, FIPS 205) — deterministic, domain "ngcore", validated
byte-for-byte against ngcore vectors (packages/sdk/test/pq-vectors.json). PQ
schemes have no key recovery, so their signature travels in the full envelope
(0x01 ‖ scheme ‖ pubkey ‖ sig). FN-DSA-512 (Falcon, 0x10) is declared by the
protocol but is not reproducible in pure JS (its keygen doesn't match go-fn-dsa);
it needs a wasm signer.
import { KeyPair, SigScheme } from '@ngchain/sdk';
const pq = KeyPair.generate(SigScheme.MLDSA44); // or SLHDSA128
console.log(pq.address); // same address ngcore derives from pq.serialize()
const txid = await ng.transfer(pq, { to, value }); // full envelope, submitted like any txcd apps/extension && npm run build # -> dist/Load apps/extension/dist as an unpacked extension at chrome://extensions.
It exposes window.ngchain (EIP-1193-shaped) to pages; connect/sign/send always
prompt the user. Keys are encrypted (PBKDF2 + AES-GCM) in storage.local; unlocked
keys live only in session memory.
ConnectButton lists the native ngchain wallet and any EVM wallet (EIP-6963).
Because ngchain uses secp256k1 (the same curve as Ethereum), an EVM key maps
deterministically to an ngchain address — so users can connect straight from
MetaMask / a WalletConnect provider and their ngchain address is derived from the
same key (linkEvmAccount, via an EIP-191 personal_sign challenge). If the EVM
wallet can sign raw hashes it transacts directly (EvmSigner → eth_sign, with a
self-verifying capability check); otherwise the UI falls back to importing the key
into the native wallet. account is always the ngchain address, either way.
import { NgWalletProvider, ConnectButton, useAccount, useBalance, useSendTransfer } from '@ngchain/react';
import { ngToBaseUnits } from '@ngchain/sdk';
function App() {
return (
// rpcUrl is required for the EVM path (submitting the EVM-signed tx to a node)
<NgWalletProvider rpcUrl="http://testnet-rpc.ngchain.org:52521">
<ConnectButton />
<Wallet />
</NgWalletProvider>
);
}
function Wallet() {
const { address, isConnected } = useAccount();
const { totalNg } = useBalance();
const [send, { pending, txid }] = useSendTransfer();
if (!isConnected) return null;
return (
<>
<div>{address}: {totalNg} NG</div>
<button disabled={pending} onClick={() => send(recipient, ngToBaseUnits('1'))}>
{pending ? 'Sending…' : 'Send 1 NG'}
</button>
{txid && <div>sent {txid.slice(0, 16)}…</div>}
</>
);
}Wallets expose window.ngchain: NgProvider with request({ method, params }) and
on(event, …). Methods: ng_requestAccounts, ng_accounts, ng_network,
ng_getBalance, ng_sendTransfer, ng_callContract, ng_deploy,
ng_signMessage, ng_rpc (read-only passthrough). Events: connect,
disconnect, accountsChanged, networkChanged.
ng_signMessage returns the signature envelope (hex), so a verifier can
recover the signer's address for any scheme — including the recovery-less PQ
schemes, whose envelope carries the public key. Verify it with the SDK:
import { verifySignedMessage } from '@ngchain/sdk';
const address = verifySignedMessage(message, envelopeHex); // bs58 address, or nullThis is the basis for wallet login: challenge → ng_signMessage → server calls
verifySignedMessage to authenticate the ngchain address, secp256k1 or PQ alike.