@ldgr/client 0.3.0 → 0.4.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 +32 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +113 -0
- package/dist/transfer.d.ts +42 -0
- package/dist/types.d.ts +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -35,6 +35,38 @@ console.log(serviceWallet.address); // safe to identify the service wallet
|
|
|
35
35
|
This is for backend/Bun service wallets only. Websites should use
|
|
36
36
|
`@ldgr/wallet-sdk` and an extension-confirmed customer wallet instead.
|
|
37
37
|
|
|
38
|
+
## Send a native or user-issued asset
|
|
39
|
+
|
|
40
|
+
`transfers.send()` resolves the asset metadata, fetches the current nonce,
|
|
41
|
+
builds and signs the transfer, submits it to the selected network, and retries
|
|
42
|
+
once if another process consumed the nonce first. Use
|
|
43
|
+
`LEDRA_TESTNET_ASSET_ID` / `LEDRA_MAINNET_ASSET_ID` from `@ldgr/protocol` for
|
|
44
|
+
the native asset or pass a user-token code.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { createLdgrClient } from "@ldgr/client";
|
|
48
|
+
import { LEDRA_MAINNET_ASSET_ID } from "@ldgr/protocol";
|
|
49
|
+
|
|
50
|
+
const client = createLdgrClient({ network: "mainnet" });
|
|
51
|
+
const result = await client.transfers.send({
|
|
52
|
+
wallet: serviceWallet,
|
|
53
|
+
asset: LEDRA_MAINNET_ASSET_ID,
|
|
54
|
+
amount: "2.50",
|
|
55
|
+
to: recipientAddress,
|
|
56
|
+
memo: "Order 1234",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (result.status === "settled") {
|
|
60
|
+
console.log(result.transactionHash, result.fee, result.feeAsset);
|
|
61
|
+
} else {
|
|
62
|
+
console.error(result.code, result.error);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Settlement failures return `status: "failed"`; they do not have a transaction
|
|
67
|
+
hash or fee because the ledger rejected them before appending a transaction.
|
|
68
|
+
Transport and unexpected response errors still throw.
|
|
69
|
+
|
|
38
70
|
```ts
|
|
39
71
|
import { createLdgrClient } from "@ldgr/client";
|
|
40
72
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type LdgrTransferResult, type SendTransferInput } from "./transfer";
|
|
1
2
|
import { type Ldgr402Requirement, type LedgerId } from "@ldgr/protocol";
|
|
2
3
|
import { type LdgrAccountTransactionPage, type LdgrClientOptions, type LdgrAsset, type Ldgr402Verification, type LdgrTransaction, type PaymentExpectation, type PaymentVerification, type SignedSubmission } from "./types";
|
|
3
4
|
export declare function createLdgrClient(options: LdgrClientOptions): {
|
|
@@ -93,6 +94,9 @@ export declare function createLdgrClient(options: LdgrClientOptions): {
|
|
|
93
94
|
readonly nowMs?: number;
|
|
94
95
|
}) => Promise<Ldgr402Verification>;
|
|
95
96
|
};
|
|
97
|
+
transfers: {
|
|
98
|
+
send: (input: SendTransferInput) => Promise<LdgrTransferResult>;
|
|
99
|
+
};
|
|
96
100
|
submitSigned: <T extends {
|
|
97
101
|
readonly transactionHash?: string;
|
|
98
102
|
}>(path: string, signed: SignedSubmission) => Promise<T>;
|
|
@@ -102,3 +106,5 @@ export { decodeMerchantLdgr402Payment, parseMerchantLdgr402Requirement } from ".
|
|
|
102
106
|
export type { LdgrAccountTransaction, LdgrAccountTransactionPage, Ldgr402Requirement, Ldgr402Verification, LdgrAsset, LdgrClientOptions, LdgrNetwork, LdgrTransaction, PaymentExpectation, PaymentVerification, SignedSubmission } from "./types";
|
|
103
107
|
export { createWebCryptoSigner, generateLocalWallet, loadLocalWallet } from "./signer";
|
|
104
108
|
export type { LdgrSigner, LocalLdgrWallet } from "./signer";
|
|
109
|
+
export { buildTransferSubmission } from "./transfer";
|
|
110
|
+
export type { BuildTransferSubmissionInput, LdgrTransferResult, SendTransferInput } from "./transfer";
|
package/dist/index.js
CHANGED
|
@@ -77,9 +77,57 @@ function queryPath(path, query) {
|
|
|
77
77
|
return encoded ? `${path}?${encoded}` : path;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
// src/transfer.ts
|
|
81
|
+
import {
|
|
82
|
+
canonicalizeLedgerAddress,
|
|
83
|
+
assertEncryptedMessageEnvelope,
|
|
84
|
+
formatAtomicAmount,
|
|
85
|
+
parseDecimalAmount
|
|
86
|
+
} from "@ldgr/protocol";
|
|
87
|
+
async function buildTransferSubmission(input) {
|
|
88
|
+
const recipient = canonicalizeLedgerAddress(input.to, input.wallet.ledgerId);
|
|
89
|
+
const amount = parseDecimalAmount(input.amount.trim(), input.assetDefinition);
|
|
90
|
+
if (amount < 0n || amount === 0n && (input.asset !== "dng" || input.encryptedMessage === undefined)) {
|
|
91
|
+
throw new RangeError("Amount must be greater than zero unless sending an encrypted Ledra message");
|
|
92
|
+
}
|
|
93
|
+
if (input.encryptedMessage !== undefined)
|
|
94
|
+
assertEncryptedMessageEnvelope(input.encryptedMessage);
|
|
95
|
+
if (!/^(0|[1-9][0-9]*)$/.test(input.nextNonce)) {
|
|
96
|
+
throw new TypeError("Ledger returned an invalid next nonce");
|
|
97
|
+
}
|
|
98
|
+
const memo = input.memo?.trim();
|
|
99
|
+
const payload = {
|
|
100
|
+
version: 1,
|
|
101
|
+
action: "transfer",
|
|
102
|
+
chain: "ldgr-v1",
|
|
103
|
+
ledgerId: input.wallet.ledgerId,
|
|
104
|
+
asset: input.asset,
|
|
105
|
+
from: input.wallet.address,
|
|
106
|
+
to: recipient,
|
|
107
|
+
amount,
|
|
108
|
+
nonce: BigInt(input.nextNonce),
|
|
109
|
+
timestamp: BigInt((input.now ?? Date.now)()),
|
|
110
|
+
publicKey: input.wallet.publicKey,
|
|
111
|
+
...memo ? { memo } : {},
|
|
112
|
+
...input.metadata === undefined ? {} : { metadata: input.metadata },
|
|
113
|
+
...input.encryptedMessage === undefined ? {} : { encryptedMessage: input.encryptedMessage }
|
|
114
|
+
};
|
|
115
|
+
const signature = await input.wallet.sign(payload);
|
|
116
|
+
return {
|
|
117
|
+
payload: {
|
|
118
|
+
...payload,
|
|
119
|
+
amount: formatAtomicAmount(amount, input.assetDefinition),
|
|
120
|
+
nonce: payload.nonce.toString(),
|
|
121
|
+
timestamp: payload.timestamp.toString()
|
|
122
|
+
},
|
|
123
|
+
signature
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
80
127
|
// src/index.ts
|
|
81
128
|
import {
|
|
82
129
|
decodeLdgr402Payment as decodeLdgr402Payment3,
|
|
130
|
+
nativeAssetDefinitionForLedger,
|
|
83
131
|
parseLdgr402Requirement as parseLdgr402Requirement3,
|
|
84
132
|
resolveLdgrNetwork as resolveLdgrNetwork2,
|
|
85
133
|
resolvePublicAssetCode
|
|
@@ -174,6 +222,18 @@ function paymentReference(metadata) {
|
|
|
174
222
|
function transactionFields(transaction) {
|
|
175
223
|
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;
|
|
176
224
|
}
|
|
225
|
+
function isNonceMismatch(error) {
|
|
226
|
+
return error instanceof LdgrClientError && error.code === "NONCE_MISMATCH";
|
|
227
|
+
}
|
|
228
|
+
function transferFailure(error) {
|
|
229
|
+
if (!(error instanceof LdgrClientError) || error.status !== 400 && error.status !== 409)
|
|
230
|
+
return;
|
|
231
|
+
return {
|
|
232
|
+
status: "failed",
|
|
233
|
+
error: error.message,
|
|
234
|
+
...error.code === undefined ? {} : { code: error.code }
|
|
235
|
+
};
|
|
236
|
+
}
|
|
177
237
|
function createLdgrClient(options) {
|
|
178
238
|
const transport = createTransport(options);
|
|
179
239
|
const ledgerId = options.network === undefined ? undefined : resolveLdgrNetwork2(options.network);
|
|
@@ -224,6 +284,57 @@ function createLdgrClient(options) {
|
|
|
224
284
|
reference: requirement.challenge
|
|
225
285
|
});
|
|
226
286
|
};
|
|
287
|
+
const sendTransfer = async (input) => {
|
|
288
|
+
if (ledgerId !== undefined && ledgerId !== input.wallet.ledgerId) {
|
|
289
|
+
throw new TypeError("signer ledgerId does not match client network");
|
|
290
|
+
}
|
|
291
|
+
const requestedAsset = input.asset.trim();
|
|
292
|
+
if (!requestedAsset)
|
|
293
|
+
throw new TypeError("asset is required");
|
|
294
|
+
const asset = resolvePublicAssetCode(requestedAsset, input.wallet.ledgerId);
|
|
295
|
+
const assetDefinition = asset === "dng" ? nativeAssetDefinitionForLedger(input.wallet.ledgerId) : await transport.get(`/api/v1/assets/${encodePathSegment(asset)}`);
|
|
296
|
+
const submit = async () => {
|
|
297
|
+
const nonce = await transport.get(`/api/v1/nonces/${encodePathSegment(input.wallet.address)}/${encodePathSegment(asset)}`);
|
|
298
|
+
const submission = await buildTransferSubmission({
|
|
299
|
+
...input,
|
|
300
|
+
asset,
|
|
301
|
+
assetDefinition,
|
|
302
|
+
nextNonce: nonce.nextNonce
|
|
303
|
+
});
|
|
304
|
+
const transaction = await transport.post("/api/v1/transfers", {
|
|
305
|
+
...submission,
|
|
306
|
+
submittedAtMs: Date.now()
|
|
307
|
+
});
|
|
308
|
+
if (typeof transaction.transactionHash !== "string" || typeof transaction.fee !== "string" || typeof transaction.feeAsset !== "string") {
|
|
309
|
+
throw new TypeError("LDGR transfer response is missing transaction hash or fee");
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
status: "settled",
|
|
313
|
+
transactionHash: transaction.transactionHash,
|
|
314
|
+
fee: transaction.fee,
|
|
315
|
+
feeAsset: transaction.feeAsset,
|
|
316
|
+
transaction
|
|
317
|
+
};
|
|
318
|
+
};
|
|
319
|
+
try {
|
|
320
|
+
return await submit();
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (isNonceMismatch(error)) {
|
|
323
|
+
try {
|
|
324
|
+
return await submit();
|
|
325
|
+
} catch (retryError) {
|
|
326
|
+
const failure2 = transferFailure(retryError);
|
|
327
|
+
if (failure2 !== undefined)
|
|
328
|
+
return failure2;
|
|
329
|
+
throw retryError;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const failure = transferFailure(error);
|
|
333
|
+
if (failure !== undefined)
|
|
334
|
+
return failure;
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
227
338
|
return {
|
|
228
339
|
ledger: {
|
|
229
340
|
get: () => transport.get("/api/v1/ledger"),
|
|
@@ -293,11 +404,13 @@ function createLdgrClient(options) {
|
|
|
293
404
|
listOwned: (address, options2) => transport.get(queryPath(`/api/v1/usernames/owned-by/${encodePathSegment(address)}`, options2 ?? {}))
|
|
294
405
|
},
|
|
295
406
|
payments: { verifyTransfer, verifyLdgr402 },
|
|
407
|
+
transfers: { send: sendTransfer },
|
|
296
408
|
submitSigned: (path, signed) => transport.post(path, { ...signed, submittedAtMs: Date.now() })
|
|
297
409
|
};
|
|
298
410
|
}
|
|
299
411
|
export {
|
|
300
412
|
LdgrClientError,
|
|
413
|
+
buildTransferSubmission,
|
|
301
414
|
createLdgrClient,
|
|
302
415
|
createWebCryptoSigner,
|
|
303
416
|
decodeMerchantLdgr402Payment,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type AssetDefinition, type JsonValue } from "@ldgr/protocol";
|
|
2
|
+
import type { LdgrSigner } from "./signer";
|
|
3
|
+
import type { LdgrTransaction } from "./types";
|
|
4
|
+
export type BuildTransferSubmissionInput = {
|
|
5
|
+
readonly wallet: LdgrSigner;
|
|
6
|
+
readonly asset: string;
|
|
7
|
+
readonly assetDefinition: AssetDefinition;
|
|
8
|
+
readonly amount: string;
|
|
9
|
+
readonly to: string;
|
|
10
|
+
readonly nextNonce: string;
|
|
11
|
+
readonly memo?: string;
|
|
12
|
+
readonly metadata?: JsonValue;
|
|
13
|
+
readonly encryptedMessage?: string;
|
|
14
|
+
readonly now?: () => number;
|
|
15
|
+
};
|
|
16
|
+
export type SendTransferInput = {
|
|
17
|
+
readonly wallet: LdgrSigner;
|
|
18
|
+
/** Public native id (`t0`/`t1`) or wire asset code. */
|
|
19
|
+
readonly asset: string;
|
|
20
|
+
readonly amount: string;
|
|
21
|
+
readonly to: string;
|
|
22
|
+
readonly memo?: string;
|
|
23
|
+
readonly metadata?: JsonValue;
|
|
24
|
+
readonly encryptedMessage?: string;
|
|
25
|
+
readonly now?: () => number;
|
|
26
|
+
};
|
|
27
|
+
export type LdgrTransferResult = {
|
|
28
|
+
readonly status: "settled";
|
|
29
|
+
readonly transactionHash: string;
|
|
30
|
+
readonly fee: string;
|
|
31
|
+
readonly feeAsset: string;
|
|
32
|
+
readonly transaction: LdgrTransaction;
|
|
33
|
+
} | {
|
|
34
|
+
readonly status: "failed";
|
|
35
|
+
/** Settlement failures happen before append, so there is no hash or fee. */
|
|
36
|
+
readonly error: string;
|
|
37
|
+
readonly code?: string;
|
|
38
|
+
};
|
|
39
|
+
export declare function buildTransferSubmission(input: BuildTransferSubmissionInput): Promise<{
|
|
40
|
+
readonly payload: Record<string, unknown>;
|
|
41
|
+
readonly signature: string;
|
|
42
|
+
}>;
|
package/dist/types.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export type LdgrTransaction = {
|
|
|
7
7
|
readonly to?: string;
|
|
8
8
|
readonly asset?: string;
|
|
9
9
|
readonly amount?: string;
|
|
10
|
+
readonly fee?: string;
|
|
11
|
+
readonly feeAsset?: string;
|
|
10
12
|
readonly metadata?: unknown;
|
|
11
13
|
readonly sequence?: string;
|
|
12
14
|
readonly transactionHash: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ldgr/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"files": ["dist", "README.md"],
|
|
13
13
|
"publishConfig": { "access": "public" },
|
|
14
14
|
"engines": { "node": ">=20.19.0" },
|
|
15
|
-
"dependencies": { "@ldgr/protocol": "^0.
|
|
15
|
+
"dependencies": { "@ldgr/protocol": "^0.4.0" },
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "rm -rf dist && bun build src/index.ts --outdir dist --target node --format esm --packages external && tsc -p tsconfig.build.json",
|
|
18
18
|
"prepack": "bun run build",
|