@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 +59 -4
- package/dist/index.d.ts +4 -2
- package/dist/index.js +75 -16
- package/dist/types.d.ts +28 -0
- package/package.json +1 -1
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
|
-
##
|
|
70
|
+
## Confidential payments
|
|
71
71
|
|
|
72
|
-
|
|
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
|
|
116
|
-
|
|
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
|
|
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:
|
|
352
|
+
nowMs: options.nowMs,
|
|
294
353
|
allowExpired: true
|
|
295
354
|
});
|
|
296
|
-
const nowMs =
|
|
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
|
|
352
|
-
if (
|
|
353
|
-
return
|
|
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
|
|
444
|
-
if (
|
|
445
|
-
return
|
|
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,
|
|
525
|
+
async listTransactions(address, options = {}) {
|
|
467
526
|
const path = queryPath(`/api/v1/explorer/addresses/${encodePathSegment(address)}/transactions`, {
|
|
468
|
-
before:
|
|
469
|
-
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,
|
|
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;
|