@ldgr/client 0.1.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 ADDED
@@ -0,0 +1,29 @@
1
+ # LDGR client
2
+
3
+ `@ldgr/client` is the transport and verification SDK for backend services and
4
+ trading systems. It reads authoritative LDGR Worker state, verifies exact
5
+ payments, and submits envelopes signed by a caller-owned signer or KMS.
6
+
7
+ It does not create a hosted wallet, persist a private key, or replace the
8
+ browser extension. Use `@ldgr/wallet-sdk` when a website needs an explicit
9
+ user confirmation in the LDGR extension.
10
+
11
+ `generateLocalWallet()` creates caller-owned key material for a service wallet
12
+ and returns its recovery key exactly to the caller. Store it in your own secret
13
+ manager or HSM integration; never log it or send it to LDGR.
14
+
15
+ ```ts
16
+ import { createLdgrClient } from "@ldgr/client";
17
+
18
+ const client = createLdgrClient({ apiBase: "https://tst.ldgr.ltd" });
19
+ const payment = await client.payments.verifyTransfer(transactionHash, {
20
+ to: merchantAddress,
21
+ asset: "dng",
22
+ amount: "2.50",
23
+ });
24
+ ```
25
+
26
+ Only fulfil an order after `payment.status === "confirmed"`, and store the
27
+ transaction hash as the idempotency key. AMM quotes are estimates: pass an
28
+ explicit minimum output in the signed swap payload and re-quote immediately
29
+ before signing.
@@ -0,0 +1,8 @@
1
+ import { type LdgrClientOptions } from "./types";
2
+ export type Transport = {
3
+ readonly get: <T>(path: string) => Promise<T>;
4
+ readonly post: <T>(path: string, body: unknown) => Promise<T>;
5
+ };
6
+ export declare function createTransport(options: LdgrClientOptions): Transport;
7
+ export declare function encodePathSegment(value: string): string;
8
+ export declare function queryPath(path: string, query: Record<string, string | number | undefined>): string;
@@ -0,0 +1,74 @@
1
+ import { type LdgrClientOptions, type LdgrTransaction, type PaymentExpectation, type PaymentVerification, type SignedSubmission } from "./types";
2
+ export declare function createLdgrClient(options: LdgrClientOptions): {
3
+ ledger: {
4
+ get: () => Promise<{
5
+ readonly ledgerId: 0 | 1;
6
+ }>;
7
+ getProtocolConfig: () => Promise<Record<string, unknown>>;
8
+ };
9
+ accounts: {
10
+ listBalances: (address: string) => Promise<{
11
+ readonly address: string;
12
+ readonly balances: readonly {
13
+ readonly asset: string;
14
+ readonly amount: string;
15
+ }[];
16
+ }>;
17
+ getBalance: (address: string, asset: string) => Promise<{
18
+ readonly address: string;
19
+ readonly asset: string;
20
+ readonly amount: string;
21
+ }>;
22
+ getNextNonce: (address: string, asset: string) => Promise<{
23
+ readonly address: string;
24
+ readonly asset: string;
25
+ readonly nextNonce: string;
26
+ }>;
27
+ };
28
+ transactions: {
29
+ get: (transactionHash: string) => Promise<LdgrTransaction>;
30
+ };
31
+ amm: {
32
+ listPools: () => Promise<{
33
+ readonly pools: readonly Record<string, unknown>[];
34
+ }>;
35
+ getPool: (poolId: string) => Promise<Record<string, unknown>>;
36
+ getPoolByPair: (assetA: string, assetB: string) => Promise<Record<string, unknown>>;
37
+ quoteSwap: (poolId: string, assetIn: string, amountIn: string) => Promise<Record<string, unknown>>;
38
+ quoteRoute: (assetIn: string, assetOut: string, amountIn: string, maxHops?: 1 | 2 | 3) => Promise<Record<string, unknown>>;
39
+ };
40
+ subscriptions: {
41
+ getPlan: (planId: string) => Promise<Record<string, unknown>>;
42
+ listPlans: (merchant: string) => Promise<{
43
+ readonly plans: readonly Record<string, unknown>[];
44
+ }>;
45
+ getEntitlement: (input: {
46
+ readonly payer: string;
47
+ readonly planId?: string;
48
+ readonly merchant?: string;
49
+ readonly tier?: string;
50
+ }) => Promise<Record<string, unknown>>;
51
+ };
52
+ objects: {
53
+ get(objectId: string): Promise<Record<string, unknown> | null>;
54
+ getByKey(namespace: string, key: string): Promise<Record<string, unknown> | null>;
55
+ };
56
+ usernames: {
57
+ get: (name: string) => Promise<Record<string, unknown>>;
58
+ getPrimary: (address: string) => Promise<Record<string, unknown>>;
59
+ listOwned: (address: string, options?: {
60
+ readonly cursor?: string;
61
+ readonly limit?: number;
62
+ }) => Promise<Record<string, unknown>>;
63
+ };
64
+ payments: {
65
+ verifyTransfer(transactionHash: string, expected: PaymentExpectation): Promise<PaymentVerification>;
66
+ };
67
+ submitSigned: <T extends {
68
+ readonly transactionHash?: string;
69
+ }>(path: string, signed: SignedSubmission) => Promise<T>;
70
+ };
71
+ export { LdgrClientError } from "./types";
72
+ export type { LdgrClientOptions, LdgrTransaction, PaymentExpectation, PaymentVerification, SignedSubmission } from "./types";
73
+ export { createWebCryptoSigner, generateLocalWallet } from "./signer";
74
+ export type { LdgrSigner, LocalLdgrWallet } from "./signer";
package/dist/index.js ADDED
@@ -0,0 +1,216 @@
1
+ // src/types.ts
2
+ class LdgrClientError extends Error {
3
+ status;
4
+ code;
5
+ constructor(status, message, code) {
6
+ super(message);
7
+ this.name = "LdgrClientError";
8
+ this.status = status;
9
+ this.code = code;
10
+ }
11
+ }
12
+
13
+ // src/client.ts
14
+ function normalizeApiBase(apiBase) {
15
+ const normalized = apiBase.trim().replace(/\/$/, "");
16
+ if (!/^https?:\/\//.test(normalized))
17
+ throw new TypeError("apiBase must be an absolute HTTP(S) URL");
18
+ return normalized;
19
+ }
20
+ function assertApiPath(path) {
21
+ if (!path.startsWith("/api/v1/") || path.includes("//") || path.includes("..")) {
22
+ throw new TypeError("path must be an /api/v1/ relative path");
23
+ }
24
+ }
25
+ async function parseJson(response) {
26
+ const body = await response.json().catch(() => {
27
+ return;
28
+ });
29
+ if (!response.ok) {
30
+ const message = typeof body?.error === "string" ? body.error : `LDGR request failed (${response.status})`;
31
+ const code = typeof body?.code === "string" ? body.code : undefined;
32
+ throw new LdgrClientError(response.status, message, code);
33
+ }
34
+ if (body === undefined || body === null || typeof body !== "object") {
35
+ throw new TypeError("LDGR response must be a JSON object");
36
+ }
37
+ return body;
38
+ }
39
+ function createTransport(options) {
40
+ const apiBase = normalizeApiBase(options.apiBase);
41
+ const request = options.fetch ?? globalThis.fetch;
42
+ if (typeof request !== "function")
43
+ throw new TypeError("fetch is required");
44
+ const call = async (path, init) => {
45
+ assertApiPath(path);
46
+ return parseJson(await request(`${apiBase}${path}`, init));
47
+ };
48
+ return {
49
+ get: (path) => call(path),
50
+ post: (path, body) => call(path, {
51
+ method: "POST",
52
+ headers: { "content-type": "application/json" },
53
+ body: JSON.stringify(body)
54
+ })
55
+ };
56
+ }
57
+ function encodePathSegment(value) {
58
+ return encodeURIComponent(value);
59
+ }
60
+ function queryPath(path, query) {
61
+ const search = new URLSearchParams;
62
+ for (const [key, value] of Object.entries(query)) {
63
+ if (value !== undefined)
64
+ search.set(key, String(value));
65
+ }
66
+ const encoded = search.toString();
67
+ return encoded ? `${path}?${encoded}` : path;
68
+ }
69
+ // src/signer.ts
70
+ import {
71
+ deriveAddress,
72
+ encodeEd25519RecoveryKey,
73
+ exportEd25519PublicKey,
74
+ generateEd25519KeyPair,
75
+ signCanonicalPayload
76
+ } from "@ldgr/protocol";
77
+ function encodeBase64Url(bytes) {
78
+ let binary = "";
79
+ for (const byte of bytes)
80
+ binary += String.fromCharCode(byte);
81
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
82
+ }
83
+ async function createWebCryptoSigner(input) {
84
+ const publicKey = encodeBase64Url(await exportEd25519PublicKey(input.publicKey));
85
+ const address = await deriveAddress(input.publicKey, input.ledgerId);
86
+ return {
87
+ address,
88
+ publicKey,
89
+ ledgerId: input.ledgerId,
90
+ async sign(payload) {
91
+ if (payload.ledgerId !== input.ledgerId)
92
+ throw new TypeError("payload ledgerId does not match signer ledgerId");
93
+ if (payload.from !== address)
94
+ throw new TypeError("payload from does not match signer address");
95
+ if (payload.publicKey !== undefined && payload.publicKey !== publicKey) {
96
+ throw new TypeError("payload publicKey does not match signer publicKey");
97
+ }
98
+ return encodeBase64Url(await signCanonicalPayload(input.privateKey, { ...payload, publicKey }));
99
+ }
100
+ };
101
+ }
102
+ async function generateLocalWallet(input) {
103
+ const keyPair = await generateEd25519KeyPair();
104
+ const signer = await createWebCryptoSigner({ ...input, ...keyPair });
105
+ const privateKeyJwk = await globalThis.crypto.subtle.exportKey("jwk", keyPair.privateKey);
106
+ return {
107
+ ...signer,
108
+ recoveryKey: encodeEd25519RecoveryKey(privateKeyJwk)
109
+ };
110
+ }
111
+
112
+ // src/index.ts
113
+ function normalizedHash(transactionHash) {
114
+ const hash = transactionHash.toLowerCase();
115
+ if (!/^[0-9a-f]{64}$/.test(hash))
116
+ throw new TypeError("transactionHash must be a 64-character hexadecimal hash");
117
+ return hash;
118
+ }
119
+ function sameDecimalAmount(actual, expected) {
120
+ const normalize = (value) => {
121
+ if (!/^(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test(value))
122
+ return null;
123
+ const [whole, fraction = ""] = value.split(".");
124
+ return `${whole}.${fraction.replace(/0+$/, "") || "0"}`;
125
+ };
126
+ return normalize(actual) !== null && normalize(actual) === normalize(expected);
127
+ }
128
+ function paymentReference(metadata) {
129
+ if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata))
130
+ return;
131
+ const value = metadata.paymentReference;
132
+ return typeof value === "string" ? value : undefined;
133
+ }
134
+ function transactionFields(transaction) {
135
+ 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;
136
+ }
137
+ function createLdgrClient(options) {
138
+ const transport = createTransport(options);
139
+ return {
140
+ ledger: {
141
+ get: () => transport.get("/api/v1/ledger"),
142
+ getProtocolConfig: () => transport.get("/api/v1/protocol/config")
143
+ },
144
+ accounts: {
145
+ listBalances: (address) => transport.get(`/api/v1/balances/${encodePathSegment(address)}`),
146
+ getBalance: (address, asset) => transport.get(`/api/v1/balances/${encodePathSegment(address)}/${encodePathSegment(asset)}`),
147
+ getNextNonce: (address, asset) => transport.get(`/api/v1/nonces/${encodePathSegment(address)}/${encodePathSegment(asset)}`)
148
+ },
149
+ transactions: {
150
+ get: (transactionHash) => transport.get(`/api/v1/transactions/${normalizedHash(transactionHash)}`)
151
+ },
152
+ amm: {
153
+ listPools: () => transport.get("/api/v1/amm/pools"),
154
+ getPool: (poolId) => transport.get(`/api/v1/amm/pools/${encodePathSegment(poolId)}`),
155
+ getPoolByPair: (assetA, assetB) => transport.get(`/api/v1/amm/pools/by-pair/${encodePathSegment(assetA)}/${encodePathSegment(assetB)}`),
156
+ quoteSwap: (poolId, assetIn, amountIn) => transport.get(queryPath(`/api/v1/amm/pools/${encodePathSegment(poolId)}/quote`, { assetIn, amountIn })),
157
+ quoteRoute: (assetIn, assetOut, amountIn, maxHops) => transport.get(queryPath("/api/v1/amm/routes/quote", { assetIn, assetOut, amountIn, maxHops }))
158
+ },
159
+ subscriptions: {
160
+ getPlan: (planId) => transport.get(`/api/v1/subscription-plans/${encodePathSegment(planId)}`),
161
+ listPlans: (merchant) => transport.get(queryPath("/api/v1/subscription-plans", { merchant })),
162
+ getEntitlement: (input) => transport.get(queryPath("/api/v1/subscriptions/entitlement", input))
163
+ },
164
+ objects: {
165
+ async get(objectId) {
166
+ try {
167
+ return await transport.get(`/api/v1/objects/${encodePathSegment(objectId)}`);
168
+ } catch (error) {
169
+ if (error instanceof LdgrClientError && error.status === 404)
170
+ return null;
171
+ throw error;
172
+ }
173
+ },
174
+ async getByKey(namespace, key) {
175
+ try {
176
+ return await transport.get(`/api/v1/objects/keys/${encodePathSegment(namespace)}/${encodePathSegment(key)}`);
177
+ } catch (error) {
178
+ if (error instanceof LdgrClientError && error.status === 404)
179
+ return null;
180
+ throw error;
181
+ }
182
+ }
183
+ },
184
+ usernames: {
185
+ get: (name) => transport.get(`/api/v1/usernames/${encodePathSegment(name)}`),
186
+ getPrimary: (address) => transport.get(`/api/v1/usernames/by-address/${encodePathSegment(address)}`),
187
+ listOwned: (address, options2) => transport.get(queryPath(`/api/v1/usernames/owned-by/${encodePathSegment(address)}`, options2 ?? {}))
188
+ },
189
+ payments: {
190
+ async verifyTransfer(transactionHash, expected) {
191
+ const hash = normalizedHash(transactionHash);
192
+ let transaction;
193
+ try {
194
+ transaction = await transport.get(`/api/v1/payments/v0.1/transactions/${hash}`);
195
+ } catch (error) {
196
+ if (error instanceof LdgrClientError && error.status === 404)
197
+ return { status: "not_found", transactionHash: hash };
198
+ throw error;
199
+ }
200
+ const fields = transactionFields(transaction);
201
+ const expectedAsset = (expected.asset ?? "dng").trim() || "dng";
202
+ 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) {
203
+ return { status: "mismatch", transactionHash: hash };
204
+ }
205
+ return { status: "confirmed", transactionHash: hash, ...fields };
206
+ }
207
+ },
208
+ submitSigned: (path, signed) => transport.post(path, signed)
209
+ };
210
+ }
211
+ export {
212
+ generateLocalWallet,
213
+ createWebCryptoSigner,
214
+ createLdgrClient,
215
+ LdgrClientError
216
+ };
@@ -0,0 +1,19 @@
1
+ import { type LedgerId } from "@ldgr/protocol";
2
+ export interface LdgrSigner {
3
+ readonly address: string;
4
+ readonly publicKey: string;
5
+ readonly ledgerId: LedgerId;
6
+ sign(payload: Record<string, unknown>): Promise<string>;
7
+ }
8
+ export interface LocalLdgrWallet extends LdgrSigner {
9
+ /** Store this only in the caller's chosen secret manager; it is never sent to LDGR. */
10
+ readonly recoveryKey: string;
11
+ }
12
+ export declare function createWebCryptoSigner(input: {
13
+ readonly ledgerId: LedgerId;
14
+ readonly privateKey: CryptoKey;
15
+ readonly publicKey: CryptoKey;
16
+ }): Promise<LdgrSigner>;
17
+ export declare function generateLocalWallet(input: {
18
+ readonly ledgerId: LedgerId;
19
+ }): Promise<LocalLdgrWallet>;
@@ -0,0 +1,47 @@
1
+ export type LedgerId = 0 | 1;
2
+ export type LdgrTransaction = {
3
+ readonly action: string;
4
+ readonly from?: string;
5
+ readonly to?: string;
6
+ readonly asset?: string;
7
+ readonly amount?: string;
8
+ readonly metadata?: unknown;
9
+ readonly sequence?: string;
10
+ readonly transactionHash: string;
11
+ readonly [key: string]: unknown;
12
+ };
13
+ export type PaymentExpectation = {
14
+ readonly to: string;
15
+ readonly asset?: string;
16
+ readonly amount: string;
17
+ readonly reference?: string;
18
+ };
19
+ export type PaymentVerification = {
20
+ readonly status: "confirmed";
21
+ readonly transactionHash: string;
22
+ readonly from: string;
23
+ readonly to: string;
24
+ readonly asset: string;
25
+ readonly amount: string;
26
+ } | {
27
+ readonly status: "not_found" | "mismatch";
28
+ readonly transactionHash: string;
29
+ };
30
+ export type SignedSubmission = {
31
+ readonly payload: Record<string, unknown>;
32
+ readonly signature: string;
33
+ readonly feeSponsor?: unknown;
34
+ };
35
+ export type LdgrClientOptions = {
36
+ readonly apiBase: string;
37
+ readonly fetch?: typeof globalThis.fetch;
38
+ };
39
+ export type LdgrClientErrorBody = {
40
+ readonly error?: unknown;
41
+ readonly code?: unknown;
42
+ };
43
+ export declare class LdgrClientError extends Error {
44
+ readonly status: number;
45
+ readonly code: string | undefined;
46
+ constructor(status: number, message: string, code: string | undefined);
47
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@ldgr/client",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "types": "./dist/index.d.ts",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ }
11
+ },
12
+ "files": ["dist", "README.md"],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "engines": {
17
+ "node": ">=20.19.0"
18
+ },
19
+ "dependencies": {
20
+ "@ldgr/protocol": "^0.1.0"
21
+ },
22
+ "scripts": {
23
+ "build": "rm -rf dist && bun build src/index.ts --outdir dist --target node --format esm --packages external && tsc -p tsconfig.build.json",
24
+ "prepack": "bun run build",
25
+ "test": "bun test",
26
+ "typecheck": "tsc -p tsconfig.json"
27
+ }
28
+ }