@ldgr/client
v0.5.0
Published
`@ldgr/client` is the transport and verification SDK for backend services and trading systems. It reads authoritative LDGR Worker state, verifies exact payments, and submits envelopes signed by a caller-owned signer or KMS.
Downloads
275
Readme
LDGR client
@ldgr/client is the transport and verification SDK for backend services and
trading systems. It reads authoritative LDGR Worker state, verifies exact
payments, and submits envelopes signed by a caller-owned signer or KMS.
It does not create a hosted wallet, persist a private key, or replace the
browser extension. Use @ldgr/wallet-sdk when a website needs an explicit
user confirmation in the LDGR extension.
Service wallets and recovery keys
generateLocalWallet() creates caller-owned key material for a service wallet.
Save its recovery key directly to your secret manager exactly once, then use
loadLocalWallet() on service startup to recreate the signer. The recovery key
is bearer private-key material: never log, commit, pass it in arguments or
browser storage, send it to LDGR, or use it for a customer wallet.
import { generateLocalWallet, loadLocalWallet } from "@ldgr/client";
// Provision once. `secretStore` represents your secret-manager or HSM workflow.
const created = await generateLocalWallet({ ledgerId: 1 });
await secretStore.writeOnce("ldgr-service-wallet", created.recoveryKey);
// On each service startup, recover the same signer without printing the key.
const serviceWallet = await loadLocalWallet({
ledgerId: 1,
recoveryKey: await secretStore.read("ldgr-service-wallet"),
});
console.log(serviceWallet.address); // safe to identify the service walletThis is for backend/Bun service wallets only. Websites should use
@ldgr/wallet-sdk and an extension-confirmed customer wallet instead.
Send a native or user-issued asset
transfers.send() resolves the asset metadata, fetches the current nonce,
builds and signs the transfer, submits it to the selected network, and retries
once if another process consumed the nonce first. Use
LEDRA_TESTNET_ASSET_ID / LEDRA_MAINNET_ASSET_ID from @ldgr/protocol for
the native asset or pass a user-token code.
import { createLdgrClient } from "@ldgr/client";
import { LEDRA_MAINNET_ASSET_ID } from "@ldgr/protocol";
const client = createLdgrClient({ network: "mainnet" });
const result = await client.transfers.send({
wallet: serviceWallet,
asset: LEDRA_MAINNET_ASSET_ID,
amount: "2.50",
to: recipientAddress,
memo: "Order 1234",
});
if (result.status === "settled") {
console.log(result.transactionHash, result.fee, result.feeAsset);
} else {
console.error(result.code, result.error);
}Settlement failures return status: "failed"; they do not have a transaction
hash or fee because the ledger rejected them before appending a transaction.
Transport and unexpected response errors still throw.
Silent payments
silentPayments.send() accepts a reusable LDGR payment code and a
LocalLdgrWallet. It signs and publishes the wallet's immutable sender-DH
public key, fetches the asset nonce, derives a fresh one-time destination, and
submits an ordinary transfer. A NONCE_MISMATCH causes one fresh derivation;
an ambiguous transport failure retries the exact signed request instead.
import {
createLdgrClient,
deriveLocalSilentPaymentIdentity,
loadLocalWallet,
} from "@ldgr/client";
const client = createLdgrClient({ network: "testnet" });
const senderWallet = await loadLocalWallet({
ledgerId: 0,
recoveryKey: await secretStore.read("ldgr-testnet-sender"),
});
// The receiver shares this code out of band; their recovery key never leaves
// their own wallet.
const receiver = deriveLocalSilentPaymentIdentity({
ledgerId: 0,
recoveryKey: await receiverSecretStore.read("ldgr-testnet-receiver"),
});
const result = await client.silentPayments.send({
wallet: senderWallet,
paymentCode: receiver.paymentCode,
asset: "t0",
amount: "2.50",
});If two byte-identical transport attempts both remain ambiguous,
SilentPaymentSubmissionUnknownError exposes the exact submission. Pass it
to client.silentPayments.retry(error.submission); do not start a fresh send,
which would use the next nonce and could duplicate a payment that already
settled.
silentPayments.scan(after, limit) returns a bounded ascending page, its raw
ledger-sequence nextAfter checkpoint, and moreAvailable. Matching requires
the receiver scan secret and belongs in wallet code such as
@ldgr/wallet-core, never on a server that does not own the recovery key. This
LDGR-specific construction is not BIP-352-compatible and remains experimental.
Wallet discovery is available on both ledgers, while reusable-code display,
first-party sending, and private-output spending remain gated pending testnet-first
operational testing and external cryptographic review.
import { createLdgrClient } from "@ldgr/client";
const client = createLdgrClient({ network: "testnet" });
const payment = await client.payments.verifyTransfer(transactionHash, {
to: merchantAddress,
asset: "t0",
amount: "2.50",
});Pass network: "testnet" | "mainnet" or the matching ledger id (0 / 1).
The client selects https://tst.ldgr.ltd or https://ldgr.ltd by default; pass
apiBase alongside network for a local deployment or proxy. Use public native
asset id t0 on testnet and t1 on mainnet. Legacy dng remains accepted for
compatibility, while raw authoritative transactions retain their signed wire
code dng.
Only fulfil an order after payment.status === "confirmed", and store the
transaction hash as the idempotency key. AMM quotes are estimates: pass an
explicit minimum output in the signed swap payload and re-quote immediately
before signing.
LDGR402 merchant verification
For a persisted LDGR402 requirement and the browser's
LDGR402-Payment header, use the protocol-aware verifier:
const verification = await client.payments.verifyLdgr402(
request.headers.get("LDGR402-Payment") ?? "",
expectedRequirement,
);
if (verification.status === "confirmed") {
// Atomically fulfill the challenge using verification.transactionHash.
}verifyLdgr402 checks the proof's ledger and challenge, rejects expired
requirements, and verifies the exact settled transfer through the
authoritative Worker. The merchant still owns challenge persistence and
idempotent fulfillment.
