@ldgr/client 0.5.0 → 0.6.1

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
@@ -8,6 +8,22 @@ It does not create a hosted wallet, persist a private key, or replace the
8
8
  browser extension. Use `@ldgr/wallet-sdk` when a website needs an explicit
9
9
  user confirmation in the LDGR extension.
10
10
 
11
+ ## Contract reads
12
+
13
+ Version `0.6.1` publishes `getLedraContract`, `getMilestoneEscrow`,
14
+ `getAtomicOffer`, and `getClaimDistribution`, together with their typed mutation
15
+ methods. It requires `@ldgr/protocol@^0.5.1` for the corresponding payload types.
16
+ Version `0.6.0` does not include these methods. Upgrade the installed client and
17
+ restart consumers before using these contract APIs.
18
+
19
+ ## Primary usernames
20
+
21
+ `client.usernames.findPrimary(address)` returns the primary username record or
22
+ `null` when the address has no active primary username. It uses the optional
23
+ lookup endpoint (`?optional=true`); lookup and transport failures still throw.
24
+ `client.usernames.getPrimary(address)` keeps the required lookup behavior and
25
+ throws on the endpoint's default 404 when no primary username exists.
26
+
11
27
  ## Service wallets and recovery keys
12
28
 
13
29
  `generateLocalWallet()` creates caller-owned key material for a service wallet.
@@ -67,9 +83,10 @@ Settlement failures return `status: "failed"`; they do not have a transaction
67
83
  hash or fee because the ledger rejected them before appending a transaction.
68
84
  Transport and unexpected response errors still throw.
69
85
 
70
- ## Silent payments
86
+ ## Confidential payments
71
87
 
72
- `silentPayments.send()` accepts a reusable LDGR payment code and a
88
+ The product term is **confidential payments**. The compatibility-stable
89
+ `silentPayments.send()` API accepts a reusable LDGR payment code and a
73
90
  `LocalLdgrWallet`. It signs and publishes the wallet's immutable sender-DH
74
91
  public key, fetches the asset nonce, derives a fresh one-time destination, and
75
92
  submits an ordinary transfer. A `NONCE_MISMATCH` causes one fresh derivation;
@@ -112,8 +129,9 @@ ledger-sequence `nextAfter` checkpoint, and `moreAvailable`. Matching requires
112
129
  the receiver scan secret and belongs in wallet code such as
113
130
  `@ldgr/wallet-core`, never on a server that does not own the recovery key. This
114
131
  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
132
+ Wallet discovery is available on both ledgers. The extension and Web Wallet
133
+ display reusable codes; the Web Wallet also sends to them. Spending or
134
+ consolidating confidential-payment outputs remains gated pending testnet-first
117
135
  operational testing and external cryptographic review.
118
136
 
119
137
  ```ts
@@ -139,6 +157,59 @@ transaction hash as the idempotency key. AMM quotes are estimates: pass an
139
157
  explicit minimum output in the signed swap payload and re-quote immediately
140
158
  before signing.
141
159
 
160
+ ## Widget and Checkout merchant verification
161
+
162
+ Available in `@ldgr/client` **0.6.0**. These methods work on servers without
163
+ DOM globals or wallet keys. Configure the client's network from your saved
164
+ order and pass only the widget's identifiers as the submitted proof:
165
+
166
+ ```ts
167
+ import { createLdgrClient } from "@ldgr/client";
168
+
169
+ const order = await orders.load(orderId); // Your persisted server order.
170
+ const client = createLdgrClient({ network: order.network });
171
+ const payment = await client.payments.verifyPayment({
172
+ transactionHash: submitted.transactionHash,
173
+ checkoutSessionId: submitted.checkoutSessionId, // Optional for wallet payments.
174
+ }, {
175
+ to: order.recipient,
176
+ asset: order.assetCode,
177
+ amount: order.grossAmount, // Exact decimal string, including Checkout's fee.
178
+ reference: order.publicReference,
179
+ });
180
+
181
+ if (payment.status === "confirmed") {
182
+ // In one database transaction: check order expiry, claim a unique
183
+ // (network, transactionHash), bind it to this order, and fulfil once.
184
+ await orders.fulfilOnce(order.id, order.network, payment.transactionHash);
185
+ }
186
+ ```
187
+
188
+ `verifyPayment` explicitly dispatches to Checkout when `checkoutSessionId` is
189
+ provided; otherwise it requires a plain transfer. It never infers Checkout
190
+ from arbitrary batch metadata or falls back after a session mismatch. Expected
191
+ `to`, `asset`, gross decimal `amount`, and `reference` must come from your server,
192
+ not from the browser. Amount comparisons use exact strings, never floating point.
193
+
194
+ For a known Checkout session, use `payments.verifyCheckout(sessionId, {
195
+ to, asset, amount, reference, transactionHash? })`. It checks the authoritative
196
+ session ID, ledger, recipient, asset, gross amount, reference, paid state, and
197
+ optional submitted hash. A confirmed result includes `sessionId`,
198
+ `transactionHash`, `from`, `to`, `asset`, gross `amount`, `merchantAmount`, and
199
+ `checkoutFee`. With the default fee, a gross `1` USDL receipt reports merchant
200
+ `0.995` USDL and Checkout fee `0.005` USDL. Do not compare the net receipt with
201
+ the gross order amount.
202
+
203
+ Non-confirmed Checkout results are `pending`, `expired`, `not_found`, or
204
+ `mismatch`. An expired funding instruction does not invalidate a paid receipt.
205
+ Network/server errors and invalid arguments throw; callers must not fulfil on
206
+ an error. `verifyTransfer` remains a strict plain-transfer verifier. It also
207
+ checks that the response hash matches the requested hash.
208
+
209
+ The SDK reads receipts; your service owns authentication, order persistence,
210
+ order expiry rules, and atomic duplicate-payment protection. Browser SDK
211
+ `getPaymentStatus` / `getCheckoutStatus` results are UX hints only.
212
+
142
213
  ## LDGR402 merchant verification
143
214
 
144
215
  For a persisted LDGR402 requirement and the browser's
@@ -159,3 +230,67 @@ if (verification.status === "confirmed") {
159
230
  requirements, and verifies the exact settled transfer through the
160
231
  authoritative Worker. The merchant still owns challenge persistence and
161
232
  idempotent fulfillment.
233
+
234
+ ## Caller-signed atomic offers
235
+
236
+ `getAtomicOffer(contractId)` reads the authoritative fixed-counterparty offer.
237
+ `openAtomicOffer`, `acceptAtomicOffer`, `cancelAtomicOffer` and `refundAtomicOffer`
238
+ submit typed caller-signed envelopes and return canonical terms plus the committed
239
+ action's `transactionHash`. The four submission types are exported as
240
+ `AtomicOfferOpenSubmission`, `AtomicOfferAcceptSubmission`,
241
+ `AtomicOfferCancelSubmission` and `AtomicOfferRefundSubmission`.
242
+
243
+ ```ts
244
+ const offer = await client.getAtomicOffer(contractId);
245
+ const receipt = await client.acceptAtomicOffer(callerSignedAcceptance);
246
+ ```
247
+
248
+ Sign the protocol's canonical payload using bigint atomic amounts, then transport
249
+ each traded amount as an exact decimal string using that asset's registry
250
+ decimals. `maxFeeAtomic` is an atomic integer string; nonce, timestamp and expiry
251
+ are integer strings. Preserve exact asset codes, signature, fields and original
252
+ open payload hash. These methods forward the supplied envelope unchanged and do
253
+ not sign, resolve aliases, replace nonces or automatically retry. A timeout leaves
254
+ the submission status unknown: read the offer or retry the identical signed
255
+ submission. Exact committed retries return the original action hash, even if a
256
+ later action changed the offer's status.
257
+
258
+ This is the protocol/API/client foundation. First-party wallet provider methods,
259
+ confirmation screens, offer-detail pages and a Utilities builder are deferred.
260
+
261
+ ## Caller-signed milestone escrow
262
+
263
+ `getMilestoneEscrow(contractId)` reads the canonical prepaid escrow.
264
+ `openMilestoneEscrow(signed)`, `releaseMilestoneEscrow(signed)` and
265
+ `refundMilestoneEscrow(signed)` submit typed caller-signed envelopes unchanged.
266
+ Opening `amount` and every `milestones[].amount` are exact decimal strings in
267
+ the escrow asset. Nonce, timestamp, expiry and native `maxFeeAtomic` are integer
268
+ strings; release `milestoneIndex` is a zero-based integer number. Every action
269
+ uses the actual escrow asset and its payer nonce lane.
270
+
271
+ Only the payer can release the next step before expiry or refund the residual
272
+ at or after expiry. Records include `releasedMilestones`, `releasedAmount`,
273
+ `remainingAmount` and `refundedAmount`; released payments remain final.
274
+ These methods never sign or rebuild submissions and never retry automatically.
275
+ After an ambiguous submission, reconcile canonical state and preserve the exact
276
+ signed envelope when retrying.
277
+
278
+ ## Caller-signed claim distributions
279
+
280
+ `getClaimDistribution(contractId)` reads the canonical prepaid distribution.
281
+ `openClaimDistribution(signed)`, `claimClaimDistribution(signed)` and
282
+ `refundClaimDistribution(signed)` forward typed caller-signed envelopes unchanged.
283
+ Opening `amount` and `allocations[].amount` are exact asset decimal strings over
284
+ HTTP; sign their protocol equivalents as bigint atomic amounts. Nonce, timestamp,
285
+ expiry and opening `maxFeeAtomic` use integer strings. Every action uses the exact
286
+ distribution asset and the signing account's nonce lane.
287
+
288
+ Opening allocates the total to 1–64 unique recipients other than the funder.
289
+ Each recipient can claim its complete zero-based `claimIndex` once before expiry,
290
+ in any order. Claims and refunds require the original `openPayloadHash`; only the
291
+ funder can refund unclaimed funds at or after expiry. Opening pays a capped native
292
+ fee; settlement is fee-free and supports known inactive assets. Records expose
293
+ per-allocation `claimed` flags, `claimedAllocations`, `claimedAmount`,
294
+ `remainingAmount` and `refundedAmount`. Mutations return the committed
295
+ `transactionHash`. These methods never sign, replace nonces or automatically
296
+ retry: preserve the exact signed envelope after an unknown outcome.
package/dist/client.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type LdgrClientOptions } from "./types";
2
2
  export type Transport = {
3
3
  readonly get: <T>(path: string) => Promise<T>;
4
+ readonly getOptional: <T>(path: string) => Promise<T | null>;
4
5
  readonly post: <T>(path: string, body: unknown) => Promise<T>;
5
6
  };
6
7
  export declare function createTransport(options: LdgrClientOptions): Transport;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,14 @@
1
+ import type { LedraContractDeploySubmission, LedraContractInvokeSubmission, LedraContractRefundSubmission } from "./types";
2
+ export type { LedraContractDeploySubmission, LedraContractInvokeSubmission, LedraContractRefundSubmission, LdgrLedraContract, LdgrLedraContractMutation } from "./types";
3
+ import type { ClaimDistributionOpenSubmission, ClaimDistributionClaimSubmission, ClaimDistributionRefundSubmission, LdgrClaimDistribution, LdgrClaimDistributionMutation } from "./types";
4
+ export type { ClaimDistributionOpenSubmission, ClaimDistributionClaimSubmission, ClaimDistributionRefundSubmission, LdgrClaimDistribution, LdgrClaimDistributionMutation } from "./types";
5
+ import type { MilestoneEscrowOpenSubmission, MilestoneEscrowReleaseSubmission, MilestoneEscrowRefundSubmission, LdgrMilestoneEscrow, LdgrMilestoneEscrowMutation } from "./types";
6
+ export type { MilestoneEscrowOpenSubmission, MilestoneEscrowReleaseSubmission, MilestoneEscrowRefundSubmission, LdgrMilestoneEscrow, LdgrMilestoneEscrowMutation } from "./types";
7
+ import type { AtomicOfferOpenSubmission, AtomicOfferAcceptSubmission, AtomicOfferCancelSubmission, AtomicOfferRefundSubmission, LdgrAtomicOffer, LdgrAtomicOfferMutation } from "./types";
8
+ export type { AtomicOfferOpenSubmission, AtomicOfferAcceptSubmission, AtomicOfferCancelSubmission, AtomicOfferRefundSubmission, LdgrAtomicOffer, LdgrAtomicOfferMutation } from "./types";
1
9
  import { type LdgrTransferResult, type SendTransferInput } from "./transfer";
2
10
  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";
11
+ 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
12
  export declare function createLdgrClient(options: LdgrClientOptions): {
5
13
  ledger: {
6
14
  get: () => Promise<{
@@ -8,6 +16,23 @@ export declare function createLdgrClient(options: LdgrClientOptions): {
8
16
  }>;
9
17
  getProtocolConfig: () => Promise<Record<string, unknown>>;
10
18
  };
19
+ getClaimDistribution: (contractId: string) => Promise<LdgrClaimDistribution>;
20
+ openClaimDistribution: (signed: ClaimDistributionOpenSubmission) => Promise<LdgrClaimDistributionMutation>;
21
+ claimClaimDistribution: (signed: ClaimDistributionClaimSubmission) => Promise<LdgrClaimDistributionMutation>;
22
+ refundClaimDistribution: (signed: ClaimDistributionRefundSubmission) => Promise<LdgrClaimDistributionMutation>;
23
+ getLedraContract: (id: string) => Promise<import("@ldgr/protocol").LedraContractRecord>;
24
+ deployLedraContract: (signed: LedraContractDeploySubmission) => Promise<import("@ldgr/protocol").LedraContractMutation>;
25
+ invokeLedraContract: (signed: LedraContractInvokeSubmission) => Promise<import("@ldgr/protocol").LedraContractMutation>;
26
+ refundLedraContract: (signed: LedraContractRefundSubmission) => Promise<import("@ldgr/protocol").LedraContractMutation>;
27
+ getMilestoneEscrow: (contractId: string) => Promise<LdgrMilestoneEscrow>;
28
+ openMilestoneEscrow: (signed: MilestoneEscrowOpenSubmission) => Promise<LdgrMilestoneEscrowMutation>;
29
+ releaseMilestoneEscrow: (signed: MilestoneEscrowReleaseSubmission) => Promise<LdgrMilestoneEscrowMutation>;
30
+ refundMilestoneEscrow: (signed: MilestoneEscrowRefundSubmission) => Promise<LdgrMilestoneEscrowMutation>;
31
+ getAtomicOffer: (contractId: string) => Promise<LdgrAtomicOffer>;
32
+ openAtomicOffer: (signed: AtomicOfferOpenSubmission) => Promise<LdgrAtomicOfferMutation>;
33
+ acceptAtomicOffer: (signed: AtomicOfferAcceptSubmission) => Promise<LdgrAtomicOfferMutation>;
34
+ cancelAtomicOffer: (signed: AtomicOfferCancelSubmission) => Promise<LdgrAtomicOfferMutation>;
35
+ refundAtomicOffer: (signed: AtomicOfferRefundSubmission) => Promise<LdgrAtomicOfferMutation>;
11
36
  accounts: {
12
37
  listBalances: (address: string) => Promise<{
13
38
  readonly address: string;
@@ -68,6 +93,7 @@ export declare function createLdgrClient(options: LdgrClientOptions): {
68
93
  usernames: {
69
94
  get: (name: string) => Promise<Record<string, unknown>>;
70
95
  getPrimary: (address: string) => Promise<Record<string, unknown>>;
96
+ findPrimary: (address: string) => Promise<Record<string, unknown> | null>;
71
97
  listOwned: (address: string, options?: {
72
98
  readonly cursor?: string;
73
99
  readonly limit?: number;
@@ -75,6 +101,8 @@ export declare function createLdgrClient(options: LdgrClientOptions): {
75
101
  };
76
102
  payments: {
77
103
  verifyTransfer: (transactionHash: string, expected: PaymentExpectation) => Promise<PaymentVerification>;
104
+ verifyCheckout: (sessionId: string, expected: CheckoutPaymentExpectation) => Promise<CheckoutPaymentVerification>;
105
+ verifyPayment: (submission: PaymentSubmission, expected: MerchantPaymentExpectation) => Promise<MerchantPaymentVerification>;
78
106
  verifyLdgr402: (proofHeader: string, requirementInput: Ldgr402Requirement, options?: {
79
107
  readonly nowMs?: number;
80
108
  }) => Promise<Ldgr402Verification>;
@@ -93,7 +121,7 @@ export declare function createLdgrClient(options: LdgrClientOptions): {
93
121
  };
94
122
  export { LdgrClientError, SilentPaymentSubmissionUnknownError } from "./types";
95
123
  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";
124
+ 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
125
  export { createWebCryptoSigner, deriveLocalSilentPaymentIdentity, generateLocalWallet, loadLocalWallet } from "./signer";
98
126
  export type { LdgrSigner, LocalLdgrWallet, LocalSilentPaymentIdentity } from "./signer";
99
127
  export { buildTransferSubmission } from "./transfer";
package/dist/index.js CHANGED
@@ -41,7 +41,7 @@ function assertApiPath(path) {
41
41
  throw new TypeError("path must be an /api/v1/ relative path");
42
42
  }
43
43
  }
44
- async function parseJson(response) {
44
+ async function parseJson(response, allowNull = false) {
45
45
  const body = await response.json().catch(() => {
46
46
  return;
47
47
  });
@@ -50,6 +50,8 @@ async function parseJson(response) {
50
50
  const code = typeof body?.code === "string" ? body.code : undefined;
51
51
  throw new LdgrClientError(response.status, message, code);
52
52
  }
53
+ if (body === null && allowNull)
54
+ return body;
53
55
  if (body === undefined || body === null || typeof body !== "object") {
54
56
  throw new TypeError("LDGR response must be a JSON object");
55
57
  }
@@ -60,12 +62,13 @@ function createTransport(options) {
60
62
  const request = options.fetch ?? globalThis.fetch;
61
63
  if (typeof request !== "function")
62
64
  throw new TypeError("fetch is required");
63
- const call = async (path, init) => {
65
+ const call = async (path, init, allowNull = false) => {
64
66
  assertApiPath(path);
65
- return parseJson(await request(`${apiBase}${path}`, init));
67
+ return parseJson(await request(`${apiBase}${path}`, init), allowNull);
66
68
  };
67
69
  return {
68
70
  get: (path) => call(path),
71
+ getOptional: (path) => call(path, undefined, true),
69
72
  post: (path, body) => call(path, {
70
73
  method: "POST",
71
74
  headers: { "content-type": "application/json" },
@@ -225,7 +228,7 @@ function decodeMerchantLdgr402Payment(headerValue) {
225
228
  // src/index.ts
226
229
  function normalizedHash(transactionHash) {
227
230
  const hash = transactionHash.toLowerCase();
228
- if (!/^[0-9a-f]{64}$/.test(hash))
231
+ if (hash.length !== 64 || !/^[0-9a-f]{64}$/.test(hash))
229
232
  throw new TypeError("transactionHash must be a 64-character hexadecimal hash");
230
233
  return hash;
231
234
  }
@@ -247,6 +250,18 @@ function paymentReference(metadata) {
247
250
  function transactionFields(transaction) {
248
251
  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
252
  }
253
+ function assertMerchantTerms(expected) {
254
+ if (typeof expected.to !== "string" || !expected.to)
255
+ throw new TypeError("to is required");
256
+ if (typeof expected.asset !== "string" || !expected.asset.trim())
257
+ throw new TypeError("asset is required");
258
+ if (typeof expected.reference !== "string" || !expected.reference || new TextEncoder().encode(expected.reference).byteLength > 128) {
259
+ throw new TypeError("reference must be 1 to 128 UTF-8 bytes");
260
+ }
261
+ if (typeof expected.amount !== "string" || !sameDecimalAmount(expected.amount, expected.amount) || !/[1-9]/.test(expected.amount)) {
262
+ throw new TypeError("amount must be a positive exact decimal string");
263
+ }
264
+ }
250
265
  function isNonceMismatch(error) {
251
266
  return error instanceof LdgrClientError && error.code === "NONCE_MISMATCH";
252
267
  }
@@ -282,18 +297,65 @@ function createLdgrClient(options) {
282
297
  throw error;
283
298
  }
284
299
  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) {
300
+ 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
301
  return { status: "mismatch", transactionHash: hash };
287
302
  }
288
303
  return { status: "confirmed", transactionHash: hash, ...fields };
289
304
  };
290
- const verifyLdgr402 = async (proofHeader, requirementInput, options2 = {}) => {
305
+ const verifyCheckout = async (sessionId, expected) => {
306
+ if (ledgerId === undefined)
307
+ throw new TypeError("network is required for Checkout verification");
308
+ if (sessionId.length !== 36 || !/^chk_[0-9a-f]{32}$/.test(sessionId))
309
+ throw new TypeError("Invalid Checkout session ID");
310
+ assertMerchantTerms(expected);
311
+ const asset = resolveAsset(expected.asset.trim());
312
+ const expectedHash = expected.transactionHash === undefined ? undefined : normalizedHash(expected.transactionHash);
313
+ let body;
314
+ try {
315
+ body = await transport.get(`/api/v1/checkout/sessions/${sessionId}`);
316
+ } catch (error) {
317
+ if (error instanceof LdgrClientError && error.status === 404)
318
+ return { status: "not_found", sessionId };
319
+ throw error;
320
+ }
321
+ const mismatch = { status: "mismatch", sessionId };
322
+ if (body === null || typeof body !== "object" || Array.isArray(body))
323
+ return mismatch;
324
+ const session = body;
325
+ 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)
326
+ return mismatch;
327
+ if (session.status === "open")
328
+ return { status: "pending", sessionId };
329
+ if (session.status === "expired")
330
+ return { status: "expired", sessionId };
331
+ 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))
332
+ return mismatch;
333
+ return {
334
+ status: "confirmed",
335
+ sessionId,
336
+ transactionHash: session.transactionHash,
337
+ from: session.guestAddress,
338
+ to: expected.to,
339
+ asset,
340
+ amount: session.amount,
341
+ merchantAmount: session.merchantAmount,
342
+ checkoutFee: session.checkoutFee
343
+ };
344
+ };
345
+ const verifyPayment = async (submission, expected) => {
346
+ if (ledgerId === undefined)
347
+ throw new TypeError("network is required for merchant payment verification");
348
+ assertMerchantTerms(expected);
349
+ const hash = normalizedHash(submission.transactionHash);
350
+ return submission.checkoutSessionId === undefined ? verifyTransfer(hash, expected) : verifyCheckout(submission.checkoutSessionId, { ...expected, transactionHash: hash });
351
+ };
352
+ const verifyLdgr402 = async (proofHeader, requirementInput, options = {}) => {
291
353
  const proof = decodeLdgr402Payment3(proofHeader);
292
354
  const requirement = parseLdgr402Requirement3(requirementInput, {
293
- nowMs: options2.nowMs,
355
+ nowMs: options.nowMs,
294
356
  allowExpired: true
295
357
  });
296
- const nowMs = options2.nowMs ?? Date.now();
358
+ const nowMs = options.nowMs ?? Date.now();
297
359
  if (ledgerId === undefined)
298
360
  throw new TypeError("network is required for LDGR402 verification");
299
361
  if (proof.ledgerId !== requirement.ledgerId || ledgerId !== requirement.ledgerId || proof.challenge !== requirement.challenge) {
@@ -348,9 +410,9 @@ function createLdgrClient(options) {
348
410
  try {
349
411
  return await submit();
350
412
  } catch (retryError) {
351
- const failure2 = transferFailure(retryError);
352
- if (failure2 !== undefined)
353
- return failure2;
413
+ const failure = transferFailure(retryError);
414
+ if (failure !== undefined)
415
+ return failure;
354
416
  throw retryError;
355
417
  }
356
418
  }
@@ -440,9 +502,9 @@ function createLdgrClient(options) {
440
502
  try {
441
503
  return await submit();
442
504
  } catch (retryError) {
443
- const failure2 = transferFailure(retryError);
444
- if (failure2 !== undefined)
445
- return failure2;
505
+ const failure = transferFailure(retryError);
506
+ if (failure !== undefined)
507
+ return failure;
446
508
  throw retryError;
447
509
  }
448
510
  }
@@ -457,16 +519,33 @@ function createLdgrClient(options) {
457
519
  get: () => transport.get("/api/v1/ledger"),
458
520
  getProtocolConfig: () => transport.get("/api/v1/protocol/config")
459
521
  },
522
+ getClaimDistribution: (contractId) => transport.get(`/api/v1/contracts/claim-distributions/${encodePathSegment(contractId)}`),
523
+ openClaimDistribution: (signed) => transport.post("/api/v1/contracts/claim-distributions", signed),
524
+ claimClaimDistribution: (signed) => transport.post(`/api/v1/contracts/claim-distributions/${encodePathSegment(signed.payload.contractId)}/claim`, signed),
525
+ refundClaimDistribution: (signed) => transport.post(`/api/v1/contracts/claim-distributions/${encodePathSegment(signed.payload.contractId)}/refund`, signed),
526
+ getLedraContract: (id) => transport.get(`/api/v1/contracts/programs/${encodePathSegment(id)}`),
527
+ deployLedraContract: (signed) => transport.post("/api/v1/contracts/programs", signed),
528
+ invokeLedraContract: (signed) => transport.post(`/api/v1/contracts/programs/${encodePathSegment(signed.payload.contractId)}/invoke`, signed),
529
+ refundLedraContract: (signed) => transport.post(`/api/v1/contracts/programs/${encodePathSegment(signed.payload.contractId)}/refund`, signed),
530
+ getMilestoneEscrow: (contractId) => transport.get(`/api/v1/contracts/milestone-escrows/${encodePathSegment(contractId)}`),
531
+ openMilestoneEscrow: (signed) => transport.post("/api/v1/contracts/milestone-escrows", signed),
532
+ releaseMilestoneEscrow: (signed) => transport.post(`/api/v1/contracts/milestone-escrows/${encodePathSegment(signed.payload.contractId)}/release`, signed),
533
+ refundMilestoneEscrow: (signed) => transport.post(`/api/v1/contracts/milestone-escrows/${encodePathSegment(signed.payload.contractId)}/refund`, signed),
534
+ getAtomicOffer: (contractId) => transport.get(`/api/v1/contracts/atomic-offers/${encodePathSegment(contractId)}`),
535
+ openAtomicOffer: (signed) => transport.post("/api/v1/contracts/atomic-offers", signed),
536
+ acceptAtomicOffer: (signed) => transport.post(`/api/v1/contracts/atomic-offers/${encodePathSegment(signed.payload.contractId)}/accept`, signed),
537
+ cancelAtomicOffer: (signed) => transport.post(`/api/v1/contracts/atomic-offers/${encodePathSegment(signed.payload.contractId)}/cancel`, signed),
538
+ refundAtomicOffer: (signed) => transport.post(`/api/v1/contracts/atomic-offers/${encodePathSegment(signed.payload.contractId)}/refund`, signed),
460
539
  accounts: {
461
540
  listBalances: (address) => transport.get(`/api/v1/balances/${encodePathSegment(address)}`),
462
541
  getBalance: (address, asset) => transport.get(`/api/v1/balances/${encodePathSegment(address)}/${encodePathSegment(resolveAsset(asset))}`),
463
542
  getNextNonce: (address, asset) => transport.get(`/api/v1/nonces/${encodePathSegment(address)}/${encodePathSegment(resolveAsset(asset))}`),
464
543
  getWalletPublicKey: (address) => transport.get(`/api/v1/wallets/${encodePathSegment(address)}/public-key`),
465
544
  registerWalletPublicKey: (address, registration) => transport.post(`/api/v1/wallets/${encodePathSegment(address)}/public-key`, registration),
466
- async listTransactions(address, options2 = {}) {
545
+ async listTransactions(address, options = {}) {
467
546
  const path = queryPath(`/api/v1/explorer/addresses/${encodePathSegment(address)}/transactions`, {
468
- before: options2.before,
469
- asset: options2.asset === undefined ? undefined : resolveAsset(options2.asset)
547
+ before: options.before,
548
+ asset: options.asset === undefined ? undefined : resolveAsset(options.asset)
470
549
  });
471
550
  try {
472
551
  return await transport.get(path);
@@ -518,9 +597,10 @@ function createLdgrClient(options) {
518
597
  usernames: {
519
598
  get: (name) => transport.get(`/api/v1/usernames/${encodePathSegment(name)}`),
520
599
  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 ?? {}))
600
+ findPrimary: (address) => transport.getOptional(`/api/v1/usernames/by-address/${encodePathSegment(address)}?optional=true`),
601
+ listOwned: (address, options) => transport.get(queryPath(`/api/v1/usernames/owned-by/${encodePathSegment(address)}`, options ?? {}))
522
602
  },
523
- payments: { verifyTransfer, verifyLdgr402 },
603
+ payments: { verifyTransfer, verifyCheckout, verifyPayment, verifyLdgr402 },
524
604
  silentPayments: {
525
605
  scan: (after = "0", limit) => transport.get(queryPath("/api/v1/silent-payments/scan", { after, limit })),
526
606
  send: sendSilentPayment,
package/dist/types.d.ts CHANGED
@@ -1,4 +1,7 @@
1
- import type { AssetBridgePolicy, LdgrNetwork } from "@ldgr/protocol";
1
+ import type { LedraContractDeployPayload, LedraContractInvokePayload, LedraContractRefundPayload, LedraContractRecord, LedraContractMutation } from '@ldgr/protocol';
2
+ import type { ClaimDistributionOpenPayload, ClaimDistributionClaimPayload, ClaimDistributionRefundPayload } from "@ldgr/protocol";
3
+ import type { MilestoneEscrowOpenPayload, MilestoneEscrowReleasePayload, MilestoneEscrowRefundPayload } from "@ldgr/protocol";
4
+ import type { AssetBridgePolicy, LdgrNetwork, AtomicOfferOpenPayload, AtomicOfferAcceptPayload, AtomicOfferCancelPayload, AtomicOfferRefundPayload } from "@ldgr/protocol";
2
5
  import type { LocalLdgrWallet } from "./signer";
3
6
  export type LedgerId = 0 | 1;
4
7
  export type { LdgrNetwork } from "@ldgr/protocol";
@@ -64,6 +67,34 @@ export type PaymentVerification = {
64
67
  readonly status: "not_found" | "mismatch";
65
68
  readonly transactionHash: string;
66
69
  };
70
+ /** Immutable merchant order terms; amount is the gross exact decimal amount. */
71
+ export type MerchantPaymentExpectation = PaymentExpectation & {
72
+ readonly asset: string;
73
+ readonly reference: string;
74
+ };
75
+ export type CheckoutPaymentExpectation = MerchantPaymentExpectation & {
76
+ readonly transactionHash?: string;
77
+ };
78
+ /** Identifiers forwarded from a widget event, never trusted order terms. */
79
+ export type PaymentSubmission = {
80
+ readonly transactionHash: string;
81
+ readonly checkoutSessionId?: string;
82
+ };
83
+ export type CheckoutPaymentVerification = {
84
+ readonly status: "confirmed";
85
+ readonly sessionId: string;
86
+ readonly transactionHash: string;
87
+ readonly from: string;
88
+ readonly to: string;
89
+ readonly asset: string;
90
+ readonly amount: string;
91
+ readonly merchantAmount: string;
92
+ readonly checkoutFee: string;
93
+ } | {
94
+ readonly status: "pending" | "expired" | "not_found" | "mismatch";
95
+ readonly sessionId: string;
96
+ };
97
+ export type MerchantPaymentVerification = PaymentVerification | CheckoutPaymentVerification;
67
98
  export type Ldgr402Verification = PaymentVerification | {
68
99
  readonly status: "expired";
69
100
  readonly transactionHash: string;
@@ -76,6 +107,38 @@ export type SignedSubmission = {
76
107
  readonly feeSponsor?: unknown;
77
108
  readonly submittedAtMs?: number;
78
109
  };
110
+ /** HTTP amounts are exact decimals; nonce/time/cap fields are integer strings. */
111
+ type AtomicOfferWire<T> = {
112
+ readonly [K in keyof T]: T[K] extends bigint ? string : T[K];
113
+ };
114
+ type AtomicOfferSubmission<T> = Omit<SignedSubmission, "payload" | "feeSponsor"> & {
115
+ readonly payload: AtomicOfferWire<T>;
116
+ };
117
+ export type AtomicOfferOpenSubmission = AtomicOfferSubmission<AtomicOfferOpenPayload>;
118
+ export type AtomicOfferAcceptSubmission = AtomicOfferSubmission<AtomicOfferAcceptPayload>;
119
+ export type AtomicOfferCancelSubmission = AtomicOfferSubmission<AtomicOfferCancelPayload>;
120
+ export type AtomicOfferRefundSubmission = AtomicOfferSubmission<AtomicOfferRefundPayload>;
121
+ export type LdgrAtomicOffer = {
122
+ readonly contractId: string;
123
+ readonly template: "atomic_offer_v1";
124
+ readonly maker: string;
125
+ readonly taker: string;
126
+ readonly offeredAsset: string;
127
+ readonly offeredAmount: string;
128
+ readonly requestedAsset: string;
129
+ readonly requestedAmount: string;
130
+ readonly expiresAt: string;
131
+ readonly status: "open" | "accepted" | "cancelled" | "refunded";
132
+ readonly expired: boolean;
133
+ readonly openPayloadHash: string;
134
+ readonly reference?: string;
135
+ readonly createdTransactionHash: string;
136
+ readonly latestTransactionHash: string;
137
+ readonly terminalTransactionHash?: string;
138
+ };
139
+ export type LdgrAtomicOfferMutation = LdgrAtomicOffer & {
140
+ readonly transactionHash: string;
141
+ };
79
142
  export type WalletPublicKeyRegistration = {
80
143
  readonly version: 1;
81
144
  readonly action: "wallet_key_register";
@@ -133,7 +196,8 @@ export type LdgrClientOptions = {
133
196
  readonly network?: LdgrNetwork;
134
197
  /** Defaults to the canonical public origin when `network` is supplied. */
135
198
  readonly apiBase?: string;
136
- readonly fetch?: typeof globalThis.fetch;
199
+ /** A Fetch-compatible function; runtime-specific helper properties are not required. */
200
+ readonly fetch?: (...args: Parameters<typeof globalThis.fetch>) => ReturnType<typeof globalThis.fetch>;
137
201
  };
138
202
  export type LdgrClientErrorBody = {
139
203
  readonly error?: unknown;
@@ -144,3 +208,76 @@ export declare class LdgrClientError extends Error {
144
208
  readonly code: string | undefined;
145
209
  constructor(status: number, message: string, code: string | undefined);
146
210
  }
211
+ /** Atomic payload amounts become exact asset decimals over HTTP, including nested steps. */
212
+ type ContractSignedWire<T> = T extends bigint ? string : T extends readonly (infer U)[] ? readonly ContractSignedWire<U>[] : T extends object ? {
213
+ readonly [K in keyof T]: ContractSignedWire<T[K]>;
214
+ } : T;
215
+ type ContractSignedSubmission<T> = Omit<SignedSubmission, "payload" | "feeSponsor"> & {
216
+ readonly payload: ContractSignedWire<T>;
217
+ };
218
+ export type MilestoneEscrowOpenSubmission = ContractSignedSubmission<MilestoneEscrowOpenPayload>;
219
+ export type MilestoneEscrowReleaseSubmission = ContractSignedSubmission<MilestoneEscrowReleasePayload>;
220
+ export type MilestoneEscrowRefundSubmission = ContractSignedSubmission<MilestoneEscrowRefundPayload>;
221
+ export type LdgrMilestoneEscrow = {
222
+ readonly contractId: string;
223
+ readonly template: "milestone_escrow_v1";
224
+ readonly payer: string;
225
+ readonly beneficiary: string;
226
+ readonly asset: string;
227
+ readonly amount: string;
228
+ readonly milestones: readonly {
229
+ readonly label: string;
230
+ readonly amount: string;
231
+ }[];
232
+ readonly releasedMilestones: number;
233
+ readonly releasedAmount: string;
234
+ readonly remainingAmount: string;
235
+ readonly refundedAmount: string;
236
+ readonly expiresAt: string;
237
+ readonly expired: boolean;
238
+ readonly status: "open" | "completed" | "refunded";
239
+ readonly openPayloadHash: string;
240
+ readonly reference?: string;
241
+ readonly createdTransactionHash: string;
242
+ readonly latestTransactionHash: string;
243
+ readonly terminalTransactionHash?: string;
244
+ };
245
+ export type LdgrMilestoneEscrowMutation = LdgrMilestoneEscrow & {
246
+ readonly transactionHash: string;
247
+ };
248
+ /** Atomic payload amounts become exact asset decimals over HTTP, including nested steps. */
249
+ export type ClaimDistributionOpenSubmission = ContractSignedSubmission<ClaimDistributionOpenPayload>;
250
+ export type ClaimDistributionClaimSubmission = ContractSignedSubmission<ClaimDistributionClaimPayload>;
251
+ export type ClaimDistributionRefundSubmission = ContractSignedSubmission<ClaimDistributionRefundPayload>;
252
+ export type LdgrClaimDistribution = {
253
+ readonly contractId: string;
254
+ readonly template: "claim_distribution_v1";
255
+ readonly funder: string;
256
+ readonly asset: string;
257
+ readonly amount: string;
258
+ readonly allocations: readonly {
259
+ readonly to: string;
260
+ readonly amount: string;
261
+ readonly claimed: boolean;
262
+ }[];
263
+ readonly claimedAllocations: number;
264
+ readonly claimedAmount: string;
265
+ readonly remainingAmount: string;
266
+ readonly refundedAmount: string;
267
+ readonly expiresAt: string;
268
+ readonly expired: boolean;
269
+ readonly status: "open" | "completed" | "refunded";
270
+ readonly openPayloadHash: string;
271
+ readonly reference?: string;
272
+ readonly createdTransactionHash: string;
273
+ readonly latestTransactionHash: string;
274
+ readonly terminalTransactionHash?: string;
275
+ };
276
+ export type LdgrClaimDistributionMutation = LdgrClaimDistribution & {
277
+ readonly transactionHash: string;
278
+ };
279
+ export type LedraContractDeploySubmission = ContractSignedSubmission<LedraContractDeployPayload>;
280
+ export type LedraContractInvokeSubmission = ContractSignedSubmission<LedraContractInvokePayload>;
281
+ export type LedraContractRefundSubmission = ContractSignedSubmission<LedraContractRefundPayload>;
282
+ export type LdgrLedraContract = LedraContractRecord;
283
+ export type LdgrLedraContractMutation = LedraContractMutation;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ldgr/client",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "exports": {
@@ -12,14 +12,14 @@
12
12
  "files": ["dist", "README.md"],
13
13
  "publishConfig": { "access": "public" },
14
14
  "engines": { "node": ">=20.19.0" },
15
- "dependencies": { "@ldgr/protocol": "^0.5.0" },
15
+ "dependencies": { "@ldgr/protocol": "^0.5.1" },
16
16
  "scripts": {
17
- "build": "rm -rf dist && bun build src/index.ts --outdir dist --target node --format esm --packages external && tsc -p tsconfig.build.json",
17
+ "build": "rm -rf dist && bun build src/index.ts --outdir dist --target node --format esm --packages external && tsrs -p tsconfig.build.json",
18
18
  "prepack": "bun run build",
19
19
  "test": "bun test",
20
- "typecheck": "tsc -p tsconfig.json"
20
+ "typecheck": "tsrs --noEmit -p tsconfig.json && tsrs --noEmit -p test/tsconfig.json"
21
21
  },
22
22
  "devDependencies": {
23
- "typescript": "^7.0.2"
23
+ "tsrs": "0.20.0-ts7.1.0-beta"
24
24
  }
25
25
  }