@ldgr/client 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -67,9 +67,10 @@ Settlement failures return `status: "failed"`; they do not have a transaction
67
67
  hash or fee because the ledger rejected them before appending a transaction.
68
68
  Transport and unexpected response errors still throw.
69
69
 
70
- ## Silent payments
70
+ ## Confidential payments
71
71
 
72
- `silentPayments.send()` accepts a reusable LDGR payment code and a
72
+ The product term is **confidential payments**. The compatibility-stable
73
+ `silentPayments.send()` API accepts a reusable LDGR payment code and a
73
74
  `LocalLdgrWallet`. It signs and publishes the wallet's immutable sender-DH
74
75
  public key, fetches the asset nonce, derives a fresh one-time destination, and
75
76
  submits an ordinary transfer. A `NONCE_MISMATCH` causes one fresh derivation;
@@ -112,8 +113,9 @@ ledger-sequence `nextAfter` checkpoint, and `moreAvailable`. Matching requires
112
113
  the receiver scan secret and belongs in wallet code such as
113
114
  `@ldgr/wallet-core`, never on a server that does not own the recovery key. This
114
115
  LDGR-specific construction is not BIP-352-compatible and remains experimental.
115
- Wallet discovery is available on both ledgers, while reusable-code display,
116
- first-party sending, and private-output spending remain gated pending testnet-first
116
+ Wallet discovery is available on both ledgers. The extension and Web Wallet
117
+ display reusable codes; the Web Wallet also sends to them. Spending or
118
+ consolidating confidential-payment outputs remains gated pending testnet-first
117
119
  operational testing and external cryptographic review.
118
120
 
119
121
  ```ts
@@ -139,6 +141,59 @@ transaction hash as the idempotency key. AMM quotes are estimates: pass an
139
141
  explicit minimum output in the signed swap payload and re-quote immediately
140
142
  before signing.
141
143
 
144
+ ## Widget and Checkout merchant verification
145
+
146
+ Available in `@ldgr/client` **0.6.0**. These methods work on servers without
147
+ DOM globals or wallet keys. Configure the client's network from your saved
148
+ order and pass only the widget's identifiers as the submitted proof:
149
+
150
+ ```ts
151
+ import { createLdgrClient } from "@ldgr/client";
152
+
153
+ const order = await orders.load(orderId); // Your persisted server order.
154
+ const client = createLdgrClient({ network: order.network });
155
+ const payment = await client.payments.verifyPayment({
156
+ transactionHash: submitted.transactionHash,
157
+ checkoutSessionId: submitted.checkoutSessionId, // Optional for wallet payments.
158
+ }, {
159
+ to: order.recipient,
160
+ asset: order.assetCode,
161
+ amount: order.grossAmount, // Exact decimal string, including Checkout's fee.
162
+ reference: order.publicReference,
163
+ });
164
+
165
+ if (payment.status === "confirmed") {
166
+ // In one database transaction: check order expiry, claim a unique
167
+ // (network, transactionHash), bind it to this order, and fulfil once.
168
+ await orders.fulfilOnce(order.id, order.network, payment.transactionHash);
169
+ }
170
+ ```
171
+
172
+ `verifyPayment` explicitly dispatches to Checkout when `checkoutSessionId` is
173
+ provided; otherwise it requires a plain transfer. It never infers Checkout
174
+ from arbitrary batch metadata or falls back after a session mismatch. Expected
175
+ `to`, `asset`, gross decimal `amount`, and `reference` must come from your server,
176
+ not from the browser. Amount comparisons use exact strings, never floating point.
177
+
178
+ For a known Checkout session, use `payments.verifyCheckout(sessionId, {
179
+ to, asset, amount, reference, transactionHash? })`. It checks the authoritative
180
+ session ID, ledger, recipient, asset, gross amount, reference, paid state, and
181
+ optional submitted hash. A confirmed result includes `sessionId`,
182
+ `transactionHash`, `from`, `to`, `asset`, gross `amount`, `merchantAmount`, and
183
+ `checkoutFee`. With the default fee, a gross `1` USDL receipt reports merchant
184
+ `0.995` USDL and Checkout fee `0.005` USDL. Do not compare the net receipt with
185
+ the gross order amount.
186
+
187
+ Non-confirmed Checkout results are `pending`, `expired`, `not_found`, or
188
+ `mismatch`. An expired funding instruction does not invalidate a paid receipt.
189
+ Network/server errors and invalid arguments throw; callers must not fulfil on
190
+ an error. `verifyTransfer` remains a strict plain-transfer verifier. It also
191
+ checks that the response hash matches the requested hash.
192
+
193
+ The SDK reads receipts; your service owns authentication, order persistence,
194
+ order expiry rules, and atomic duplicate-payment protection. Browser SDK
195
+ `getPaymentStatus` / `getCheckoutStatus` results are UX hints only.
196
+
142
197
  ## LDGR402 merchant verification
143
198
 
144
199
  For a persisted LDGR402 requirement and the browser's
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type LdgrTransferResult, type SendTransferInput } from "./transfer";
2
2
  import { type Ldgr402Requirement } from "@ldgr/protocol";
3
- import { type LdgrAccountTransactionPage, type LdgrClientOptions, type LdgrAsset, type Ldgr402Verification, type LdgrTransaction, type PaymentExpectation, type PaymentVerification, type SignedSubmission, type SilentPaymentSendInput, type SilentPaymentScanPage, type WalletPublicKeyRecord, type WalletPublicKeyRegistration } from "./types";
3
+ import { type LdgrAccountTransactionPage, type LdgrClientOptions, type LdgrAsset, type Ldgr402Verification, type LdgrTransaction, type PaymentExpectation, type PaymentVerification, type CheckoutPaymentExpectation, type CheckoutPaymentVerification, type MerchantPaymentExpectation, type MerchantPaymentVerification, type PaymentSubmission, type SignedSubmission, type SilentPaymentSendInput, type SilentPaymentScanPage, type WalletPublicKeyRecord, type WalletPublicKeyRegistration } from "./types";
4
4
  export declare function createLdgrClient(options: LdgrClientOptions): {
5
5
  ledger: {
6
6
  get: () => Promise<{
@@ -75,6 +75,8 @@ export declare function createLdgrClient(options: LdgrClientOptions): {
75
75
  };
76
76
  payments: {
77
77
  verifyTransfer: (transactionHash: string, expected: PaymentExpectation) => Promise<PaymentVerification>;
78
+ verifyCheckout: (sessionId: string, expected: CheckoutPaymentExpectation) => Promise<CheckoutPaymentVerification>;
79
+ verifyPayment: (submission: PaymentSubmission, expected: MerchantPaymentExpectation) => Promise<MerchantPaymentVerification>;
78
80
  verifyLdgr402: (proofHeader: string, requirementInput: Ldgr402Requirement, options?: {
79
81
  readonly nowMs?: number;
80
82
  }) => Promise<Ldgr402Verification>;
@@ -93,7 +95,7 @@ export declare function createLdgrClient(options: LdgrClientOptions): {
93
95
  };
94
96
  export { LdgrClientError, SilentPaymentSubmissionUnknownError } from "./types";
95
97
  export { decodeMerchantLdgr402Payment, parseMerchantLdgr402Requirement } from "./ldgr402";
96
- export type { LdgrAccountTransaction, LdgrAccountTransactionPage, Ldgr402Requirement, Ldgr402Verification, LdgrAsset, LdgrClientOptions, LdgrNetwork, LdgrTransaction, PaymentExpectation, PaymentVerification, SignedSubmission, SilentPaymentScanPage, SilentPaymentScanRecord, SilentPaymentSendInput, WalletPublicKeyRecord, WalletPublicKeyRegistration } from "./types";
98
+ export type { CheckoutPaymentExpectation, CheckoutPaymentVerification, MerchantPaymentExpectation, MerchantPaymentVerification, PaymentSubmission, LdgrAccountTransaction, LdgrAccountTransactionPage, Ldgr402Requirement, Ldgr402Verification, LdgrAsset, LdgrClientOptions, LdgrNetwork, LdgrTransaction, PaymentExpectation, PaymentVerification, SignedSubmission, SilentPaymentScanPage, SilentPaymentScanRecord, SilentPaymentSendInput, WalletPublicKeyRecord, WalletPublicKeyRegistration } from "./types";
97
99
  export { createWebCryptoSigner, deriveLocalSilentPaymentIdentity, generateLocalWallet, loadLocalWallet } from "./signer";
98
100
  export type { LdgrSigner, LocalLdgrWallet, LocalSilentPaymentIdentity } from "./signer";
99
101
  export { buildTransferSubmission } from "./transfer";
package/dist/index.js CHANGED
@@ -225,7 +225,7 @@ function decodeMerchantLdgr402Payment(headerValue) {
225
225
  // src/index.ts
226
226
  function normalizedHash(transactionHash) {
227
227
  const hash = transactionHash.toLowerCase();
228
- if (!/^[0-9a-f]{64}$/.test(hash))
228
+ if (hash.length !== 64 || !/^[0-9a-f]{64}$/.test(hash))
229
229
  throw new TypeError("transactionHash must be a 64-character hexadecimal hash");
230
230
  return hash;
231
231
  }
@@ -247,6 +247,18 @@ function paymentReference(metadata) {
247
247
  function transactionFields(transaction) {
248
248
  return typeof transaction.from === "string" && typeof transaction.to === "string" && typeof transaction.asset === "string" && typeof transaction.amount === "string" ? { from: transaction.from, to: transaction.to, asset: transaction.asset, amount: transaction.amount } : null;
249
249
  }
250
+ function assertMerchantTerms(expected) {
251
+ if (typeof expected.to !== "string" || !expected.to)
252
+ throw new TypeError("to is required");
253
+ if (typeof expected.asset !== "string" || !expected.asset.trim())
254
+ throw new TypeError("asset is required");
255
+ if (typeof expected.reference !== "string" || !expected.reference || new TextEncoder().encode(expected.reference).byteLength > 128) {
256
+ throw new TypeError("reference must be 1 to 128 UTF-8 bytes");
257
+ }
258
+ if (typeof expected.amount !== "string" || !sameDecimalAmount(expected.amount, expected.amount) || !/[1-9]/.test(expected.amount)) {
259
+ throw new TypeError("amount must be a positive exact decimal string");
260
+ }
261
+ }
250
262
  function isNonceMismatch(error) {
251
263
  return error instanceof LdgrClientError && error.code === "NONCE_MISMATCH";
252
264
  }
@@ -282,18 +294,65 @@ function createLdgrClient(options) {
282
294
  throw error;
283
295
  }
284
296
  const fields = transactionFields(transaction);
285
- if (transaction.action !== "transfer" || fields === null || fields.to !== expected.to || fields.asset !== expectedAsset || !sameDecimalAmount(fields.amount, expected.amount) || expected.reference !== undefined && paymentReference(transaction.metadata) !== expected.reference) {
297
+ if (transaction.transactionHash !== hash || transaction.action !== "transfer" || fields === null || fields.to !== expected.to || fields.asset !== expectedAsset || !sameDecimalAmount(fields.amount, expected.amount) || expected.reference !== undefined && paymentReference(transaction.metadata) !== expected.reference) {
286
298
  return { status: "mismatch", transactionHash: hash };
287
299
  }
288
300
  return { status: "confirmed", transactionHash: hash, ...fields };
289
301
  };
290
- const verifyLdgr402 = async (proofHeader, requirementInput, options2 = {}) => {
302
+ const verifyCheckout = async (sessionId, expected) => {
303
+ if (ledgerId === undefined)
304
+ throw new TypeError("network is required for Checkout verification");
305
+ if (sessionId.length !== 36 || !/^chk_[0-9a-f]{32}$/.test(sessionId))
306
+ throw new TypeError("Invalid Checkout session ID");
307
+ assertMerchantTerms(expected);
308
+ const asset = resolveAsset(expected.asset.trim());
309
+ const expectedHash = expected.transactionHash === undefined ? undefined : normalizedHash(expected.transactionHash);
310
+ let body;
311
+ try {
312
+ body = await transport.get(`/api/v1/checkout/sessions/${sessionId}`);
313
+ } catch (error) {
314
+ if (error instanceof LdgrClientError && error.status === 404)
315
+ return { status: "not_found", sessionId };
316
+ throw error;
317
+ }
318
+ const mismatch = { status: "mismatch", sessionId };
319
+ if (body === null || typeof body !== "object" || Array.isArray(body))
320
+ return mismatch;
321
+ const session = body;
322
+ if (session.id !== sessionId || session.ledgerId !== ledgerId || session.recipient !== expected.to || session.asset !== asset || typeof session.amount !== "string" || !sameDecimalAmount(session.amount, expected.amount) || session.paymentReference !== expected.reference)
323
+ return mismatch;
324
+ if (session.status === "open")
325
+ return { status: "pending", sessionId };
326
+ if (session.status === "expired")
327
+ return { status: "expired", sessionId };
328
+ if (session.status !== "paid" || typeof session.transactionHash !== "string" || session.transactionHash.length !== 64 || !/^[0-9a-f]{64}$/.test(session.transactionHash) || expectedHash !== undefined && session.transactionHash !== expectedHash || typeof session.guestAddress !== "string" || !session.guestAddress || typeof session.merchantAmount !== "string" || !sameDecimalAmount(session.merchantAmount, session.merchantAmount) || typeof session.checkoutFee !== "string" || !sameDecimalAmount(session.checkoutFee, session.checkoutFee))
329
+ return mismatch;
330
+ return {
331
+ status: "confirmed",
332
+ sessionId,
333
+ transactionHash: session.transactionHash,
334
+ from: session.guestAddress,
335
+ to: expected.to,
336
+ asset,
337
+ amount: session.amount,
338
+ merchantAmount: session.merchantAmount,
339
+ checkoutFee: session.checkoutFee
340
+ };
341
+ };
342
+ const verifyPayment = async (submission, expected) => {
343
+ if (ledgerId === undefined)
344
+ throw new TypeError("network is required for merchant payment verification");
345
+ assertMerchantTerms(expected);
346
+ const hash = normalizedHash(submission.transactionHash);
347
+ return submission.checkoutSessionId === undefined ? verifyTransfer(hash, expected) : verifyCheckout(submission.checkoutSessionId, { ...expected, transactionHash: hash });
348
+ };
349
+ const verifyLdgr402 = async (proofHeader, requirementInput, options = {}) => {
291
350
  const proof = decodeLdgr402Payment3(proofHeader);
292
351
  const requirement = parseLdgr402Requirement3(requirementInput, {
293
- nowMs: options2.nowMs,
352
+ nowMs: options.nowMs,
294
353
  allowExpired: true
295
354
  });
296
- const nowMs = options2.nowMs ?? Date.now();
355
+ const nowMs = options.nowMs ?? Date.now();
297
356
  if (ledgerId === undefined)
298
357
  throw new TypeError("network is required for LDGR402 verification");
299
358
  if (proof.ledgerId !== requirement.ledgerId || ledgerId !== requirement.ledgerId || proof.challenge !== requirement.challenge) {
@@ -348,9 +407,9 @@ function createLdgrClient(options) {
348
407
  try {
349
408
  return await submit();
350
409
  } catch (retryError) {
351
- const failure2 = transferFailure(retryError);
352
- if (failure2 !== undefined)
353
- return failure2;
410
+ const failure = transferFailure(retryError);
411
+ if (failure !== undefined)
412
+ return failure;
354
413
  throw retryError;
355
414
  }
356
415
  }
@@ -440,9 +499,9 @@ function createLdgrClient(options) {
440
499
  try {
441
500
  return await submit();
442
501
  } catch (retryError) {
443
- const failure2 = transferFailure(retryError);
444
- if (failure2 !== undefined)
445
- return failure2;
502
+ const failure = transferFailure(retryError);
503
+ if (failure !== undefined)
504
+ return failure;
446
505
  throw retryError;
447
506
  }
448
507
  }
@@ -463,10 +522,10 @@ function createLdgrClient(options) {
463
522
  getNextNonce: (address, asset) => transport.get(`/api/v1/nonces/${encodePathSegment(address)}/${encodePathSegment(resolveAsset(asset))}`),
464
523
  getWalletPublicKey: (address) => transport.get(`/api/v1/wallets/${encodePathSegment(address)}/public-key`),
465
524
  registerWalletPublicKey: (address, registration) => transport.post(`/api/v1/wallets/${encodePathSegment(address)}/public-key`, registration),
466
- async listTransactions(address, options2 = {}) {
525
+ async listTransactions(address, options = {}) {
467
526
  const path = queryPath(`/api/v1/explorer/addresses/${encodePathSegment(address)}/transactions`, {
468
- before: options2.before,
469
- asset: options2.asset === undefined ? undefined : resolveAsset(options2.asset)
527
+ before: options.before,
528
+ asset: options.asset === undefined ? undefined : resolveAsset(options.asset)
470
529
  });
471
530
  try {
472
531
  return await transport.get(path);
@@ -518,9 +577,9 @@ function createLdgrClient(options) {
518
577
  usernames: {
519
578
  get: (name) => transport.get(`/api/v1/usernames/${encodePathSegment(name)}`),
520
579
  getPrimary: (address) => transport.get(`/api/v1/usernames/by-address/${encodePathSegment(address)}`),
521
- listOwned: (address, options2) => transport.get(queryPath(`/api/v1/usernames/owned-by/${encodePathSegment(address)}`, options2 ?? {}))
580
+ listOwned: (address, options) => transport.get(queryPath(`/api/v1/usernames/owned-by/${encodePathSegment(address)}`, options ?? {}))
522
581
  },
523
- payments: { verifyTransfer, verifyLdgr402 },
582
+ payments: { verifyTransfer, verifyCheckout, verifyPayment, verifyLdgr402 },
524
583
  silentPayments: {
525
584
  scan: (after = "0", limit) => transport.get(queryPath("/api/v1/silent-payments/scan", { after, limit })),
526
585
  send: sendSilentPayment,
package/dist/types.d.ts CHANGED
@@ -64,6 +64,34 @@ export type PaymentVerification = {
64
64
  readonly status: "not_found" | "mismatch";
65
65
  readonly transactionHash: string;
66
66
  };
67
+ /** Immutable merchant order terms; amount is the gross exact decimal amount. */
68
+ export type MerchantPaymentExpectation = PaymentExpectation & {
69
+ readonly asset: string;
70
+ readonly reference: string;
71
+ };
72
+ export type CheckoutPaymentExpectation = MerchantPaymentExpectation & {
73
+ readonly transactionHash?: string;
74
+ };
75
+ /** Identifiers forwarded from a widget event, never trusted order terms. */
76
+ export type PaymentSubmission = {
77
+ readonly transactionHash: string;
78
+ readonly checkoutSessionId?: string;
79
+ };
80
+ export type CheckoutPaymentVerification = {
81
+ readonly status: "confirmed";
82
+ readonly sessionId: string;
83
+ readonly transactionHash: string;
84
+ readonly from: string;
85
+ readonly to: string;
86
+ readonly asset: string;
87
+ readonly amount: string;
88
+ readonly merchantAmount: string;
89
+ readonly checkoutFee: string;
90
+ } | {
91
+ readonly status: "pending" | "expired" | "not_found" | "mismatch";
92
+ readonly sessionId: string;
93
+ };
94
+ export type MerchantPaymentVerification = PaymentVerification | CheckoutPaymentVerification;
67
95
  export type Ldgr402Verification = PaymentVerification | {
68
96
  readonly status: "expired";
69
97
  readonly transactionHash: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ldgr/client",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "exports": {