@atumlabs/payment-gateway-client 2.0.0 → 2.2.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/CHANGELOG.md +49 -0
- package/README.md +214 -58
- package/THIRD-PARTY-NOTICES.txt +45 -0
- package/dist/cli/payment-status.js +157 -46
- package/dist/cli/quote-status.js +117 -45
- package/dist/cli/request-quote.js +118 -46
- package/dist/cli/send-payment.js +332 -94
- package/dist/index.d.ts +180 -29
- package/dist/index.js +268 -71
- package/docs/idempotency.md +142 -0
- package/package.json +14 -4
- /package/{LICENSE.md → LICENSE} +0 -0
package/dist/cli/quote-status.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2026 Atum Labs, Inc. All rights reserved.
|
|
4
|
+
* Proprietary and confidential.
|
|
5
|
+
* Access and use are governed by the LICENSE file distributed with this package.
|
|
6
|
+
* Do not remove, alter, or obscure this notice.
|
|
7
|
+
*/
|
|
2
8
|
"use strict";
|
|
3
9
|
var __create = Object.create;
|
|
4
10
|
var __defProp = Object.defineProperty;
|
|
@@ -243,12 +249,14 @@ var require_utils = __commonJS({
|
|
|
243
249
|
"../schemas/declarations/PaymentRequest/v1/bindings/typescript/sender-auth/dist/utils.js"(exports2) {
|
|
244
250
|
"use strict";
|
|
245
251
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
246
|
-
exports2.SOLANA_REPLAY_SLOTS = exports2.PERMIT_NONCE_DOMAIN = exports2.REPLAY_HORIZON_SECS = void 0;
|
|
252
|
+
exports2.REQUEST_ID_DOMAIN = exports2.SOLANA_REPLAY_SLOTS = exports2.PERMIT_NONCE_DOMAIN = exports2.REPLAY_HORIZON_SECS = void 0;
|
|
247
253
|
exports2.resolvePermitDeadline = resolvePermitDeadline;
|
|
248
254
|
exports2.generatePermitNonce = generatePermitNonce;
|
|
249
255
|
exports2.resolvePermitNonce = resolvePermitNonce;
|
|
256
|
+
exports2.canonicalDepositor = canonicalDepositor;
|
|
250
257
|
exports2.deterministicPermitNonce = deterministicPermitNonce2;
|
|
251
258
|
exports2.deterministicSolanaNonce = deterministicSolanaNonce2;
|
|
259
|
+
exports2.deterministicRequestId = deterministicRequestId;
|
|
252
260
|
exports2.sameAddress = sameAddress;
|
|
253
261
|
exports2.assertKeyMatchesSender = assertKeyMatchesSender2;
|
|
254
262
|
exports2.assertToken = assertToken;
|
|
@@ -293,6 +301,20 @@ var require_utils = __commonJS({
|
|
|
293
301
|
const bucket = (seed >> 8n) % (1n << SOLANA_BUCKET_BITS);
|
|
294
302
|
return bucket << 32n | slot;
|
|
295
303
|
}
|
|
304
|
+
exports2.REQUEST_ID_DOMAIN = "atum-request:v1:";
|
|
305
|
+
function deterministicRequestId(params) {
|
|
306
|
+
if (!params.anchor) {
|
|
307
|
+
throw new Error("sender_auth: a per-intent anchor is required to derive a deterministic request_id (a random one would silently break idempotency and let a retry double-charge)");
|
|
308
|
+
}
|
|
309
|
+
if (!params.account) {
|
|
310
|
+
throw new Error("sender_auth: account is required to derive a deterministic request_id");
|
|
311
|
+
}
|
|
312
|
+
if (!/^[A-Za-z0-9_-]*$/.test(params.prefix)) {
|
|
313
|
+
throw new Error(`sender_auth: request_id prefix must contain only letters, digits, '_' or '-' (got ${JSON.stringify(params.prefix)})`);
|
|
314
|
+
}
|
|
315
|
+
const digest = (0, ethers_1.keccak256)((0, ethers_1.toUtf8Bytes)(`${exports2.REQUEST_ID_DOMAIN}${params.anchor}:${canonicalDepositor(params.account)}`));
|
|
316
|
+
return `${params.prefix}${digest.slice(2, 26)}`;
|
|
317
|
+
}
|
|
296
318
|
function sameAddress(a, b) {
|
|
297
319
|
if (a.startsWith("0x") && b.startsWith("0x")) {
|
|
298
320
|
return a.toLowerCase() === b.toLowerCase();
|
|
@@ -884,7 +906,7 @@ var require_evm = __commonJS({
|
|
|
884
906
|
exports2.evmAdapter = {
|
|
885
907
|
async buildAuthMessage({ sourceAsset, maxSourceAmount, sourceDefaults, deadline, nonce }) {
|
|
886
908
|
if (nonce === void 0) {
|
|
887
|
-
throw new Error("sender_auth: buildAuthMessage requires an explicit nonce \u2014 derive it deterministically from a stable request id (e.g.
|
|
909
|
+
throw new Error("sender_auth: buildAuthMessage requires an explicit nonce \u2014 derive it deterministically from a stable request id (e.g. an order id) so retries reproduce the same permit; pass generatePermitNonce() only for genuinely one-shot builds");
|
|
888
910
|
}
|
|
889
911
|
if (deadline === void 0) {
|
|
890
912
|
throw new Error("sender_auth: buildAuthMessage requires an explicit permit deadline (absolute Unix seconds) \u2014 the low-level builder does not default it; derive it from the payment's fulfillment/settlement deadline in the calling layer (pgc/mppx)");
|
|
@@ -912,6 +934,10 @@ var require_evm = __commonJS({
|
|
|
912
934
|
throw new Error("EVM adapter sign(): signed_messages[i].message_prehash is required");
|
|
913
935
|
}
|
|
914
936
|
const prehash = JSON.parse(signedMessage.message_prehash);
|
|
937
|
+
const prehashDigest = (0, evm_escrow_encoding_1.hashPermitWitnessTransferFrom)(prehash);
|
|
938
|
+
if (prehashDigest.toLowerCase() !== signedMessage.message.toLowerCase()) {
|
|
939
|
+
throw new Error(`EVM adapter sign(): message_prehash implies digest ${prehashDigest} but message is ${signedMessage.message}`);
|
|
940
|
+
}
|
|
915
941
|
const wallet = new ethers_1.Wallet(privateKey);
|
|
916
942
|
return wallet.signTypedData(prehash.domain, prehash.types, prehash.message);
|
|
917
943
|
},
|
|
@@ -998,7 +1024,7 @@ var require_tron = __commonJS({
|
|
|
998
1024
|
exports2.tronAdapter = {
|
|
999
1025
|
async buildAuthMessage({ sourceAsset, maxSourceAmount, sourceDefaults, deadline, nonce }) {
|
|
1000
1026
|
if (nonce === void 0) {
|
|
1001
|
-
throw new Error("sender_auth: buildAuthMessage requires an explicit nonce \u2014 derive it deterministically from a stable request id (e.g.
|
|
1027
|
+
throw new Error("sender_auth: buildAuthMessage requires an explicit nonce \u2014 derive it deterministically from a stable request id (e.g. an order id) so retries reproduce the same permit; pass generatePermitNonce() only for genuinely one-shot builds");
|
|
1002
1028
|
}
|
|
1003
1029
|
if (deadline === void 0) {
|
|
1004
1030
|
throw new Error("sender_auth: buildAuthMessage requires an explicit permit deadline (absolute Unix seconds) \u2014 the low-level builder does not default it; derive it from the payment's fulfillment/settlement deadline in the calling layer (pgc/mppx)");
|
|
@@ -1021,9 +1047,19 @@ var require_tron = __commonJS({
|
|
|
1021
1047
|
const messageHash = (0, tvm_escrow_encoding_1.hashTvmPermitWitnessTransferFrom)(messagePrehash);
|
|
1022
1048
|
return { messageScheme: "EVM_PERMIT2", messageHash, messagePrehash: JSON.stringify(messagePrehash) };
|
|
1023
1049
|
},
|
|
1050
|
+
// Signs the TIP-712 typed data, not the bare digest, so a signer can inspect the fields it is
|
|
1051
|
+
// authorizing (Turnkey policies read `eth.eip_712.message[...]`).
|
|
1024
1052
|
async sign(signedMessage, privateKey) {
|
|
1053
|
+
if (!signedMessage.message_prehash) {
|
|
1054
|
+
throw new Error("Tron adapter sign(): signed_messages[i].message_prehash is required");
|
|
1055
|
+
}
|
|
1056
|
+
const prehash = JSON.parse(signedMessage.message_prehash);
|
|
1057
|
+
const prehashDigest = (0, tvm_escrow_encoding_1.hashTvmPermitWitnessTransferFrom)(prehash);
|
|
1058
|
+
if (prehashDigest.toLowerCase() !== signedMessage.message.toLowerCase()) {
|
|
1059
|
+
throw new Error(`Tron adapter sign(): message_prehash implies digest ${prehashDigest} but message is ${signedMessage.message}`);
|
|
1060
|
+
}
|
|
1025
1061
|
const wallet = new ethers_1.Wallet(privateKey.startsWith("0x") ? privateKey : "0x" + privateKey);
|
|
1026
|
-
return wallet.
|
|
1062
|
+
return wallet.signTypedData(prehash.domain, prehash.types, prehash.message);
|
|
1027
1063
|
},
|
|
1028
1064
|
recoverSigner(signed, source) {
|
|
1029
1065
|
let recovered;
|
|
@@ -2445,7 +2481,7 @@ var Interceptors = class {
|
|
|
2445
2481
|
}
|
|
2446
2482
|
};
|
|
2447
2483
|
var OpenAPI = {
|
|
2448
|
-
BASE: "https://
|
|
2484
|
+
BASE: "https://payment-gw.production-mainnet.atum.xyz",
|
|
2449
2485
|
CREDENTIALS: "include",
|
|
2450
2486
|
ENCODE_PATH: void 0,
|
|
2451
2487
|
HEADERS: void 0,
|
|
@@ -2462,8 +2498,8 @@ var OpenAPI = {
|
|
|
2462
2498
|
|
|
2463
2499
|
// src/generated/core/BaseHttpRequest.ts
|
|
2464
2500
|
var BaseHttpRequest = class {
|
|
2465
|
-
constructor(
|
|
2466
|
-
this.config =
|
|
2501
|
+
constructor(config2) {
|
|
2502
|
+
this.config = config2;
|
|
2467
2503
|
}
|
|
2468
2504
|
};
|
|
2469
2505
|
|
|
@@ -2614,15 +2650,15 @@ var getQueryString = (params) => {
|
|
|
2614
2650
|
Object.entries(params).forEach(([key, value]) => encodePair(key, value));
|
|
2615
2651
|
return qs.length ? `?${qs.join("&")}` : "";
|
|
2616
2652
|
};
|
|
2617
|
-
var getUrl = (
|
|
2618
|
-
const encoder =
|
|
2619
|
-
const path = options.url.replace("{api-version}",
|
|
2653
|
+
var getUrl = (config2, options) => {
|
|
2654
|
+
const encoder = config2.ENCODE_PATH || encodeURI;
|
|
2655
|
+
const path = options.url.replace("{api-version}", config2.VERSION).replace(/{(.*?)}/g, (substring, group) => {
|
|
2620
2656
|
if (options.path?.hasOwnProperty(group)) {
|
|
2621
2657
|
return encoder(String(options.path[group]));
|
|
2622
2658
|
}
|
|
2623
2659
|
return substring;
|
|
2624
2660
|
});
|
|
2625
|
-
const url =
|
|
2661
|
+
const url = config2.BASE + path;
|
|
2626
2662
|
return options.query ? url + getQueryString(options.query) : url;
|
|
2627
2663
|
};
|
|
2628
2664
|
var getFormData = (options) => {
|
|
@@ -2652,12 +2688,12 @@ var resolve = async (options, resolver) => {
|
|
|
2652
2688
|
}
|
|
2653
2689
|
return resolver;
|
|
2654
2690
|
};
|
|
2655
|
-
var getHeaders = async (
|
|
2691
|
+
var getHeaders = async (config2, options) => {
|
|
2656
2692
|
const [token, username, password, additionalHeaders] = await Promise.all([
|
|
2657
|
-
resolve(options,
|
|
2658
|
-
resolve(options,
|
|
2659
|
-
resolve(options,
|
|
2660
|
-
resolve(options,
|
|
2693
|
+
resolve(options, config2.TOKEN),
|
|
2694
|
+
resolve(options, config2.USERNAME),
|
|
2695
|
+
resolve(options, config2.PASSWORD),
|
|
2696
|
+
resolve(options, config2.HEADERS)
|
|
2661
2697
|
]);
|
|
2662
2698
|
const headers = Object.entries({
|
|
2663
2699
|
Accept: "application/json",
|
|
@@ -2697,7 +2733,7 @@ var getRequestBody = (options) => {
|
|
|
2697
2733
|
}
|
|
2698
2734
|
return void 0;
|
|
2699
2735
|
};
|
|
2700
|
-
var sendRequest = async (
|
|
2736
|
+
var sendRequest = async (config2, options, url, body, formData, headers, onCancel, axiosClient) => {
|
|
2701
2737
|
const controller = new AbortController();
|
|
2702
2738
|
let requestConfig = {
|
|
2703
2739
|
data: body ?? formData,
|
|
@@ -2705,10 +2741,10 @@ var sendRequest = async (config, options, url, body, formData, headers, onCancel
|
|
|
2705
2741
|
method: options.method,
|
|
2706
2742
|
signal: controller.signal,
|
|
2707
2743
|
url,
|
|
2708
|
-
withCredentials:
|
|
2744
|
+
withCredentials: config2.WITH_CREDENTIALS
|
|
2709
2745
|
};
|
|
2710
2746
|
onCancel(() => controller.abort());
|
|
2711
|
-
for (const fn of
|
|
2747
|
+
for (const fn of config2.interceptors.request._fns) {
|
|
2712
2748
|
requestConfig = await fn(requestConfig);
|
|
2713
2749
|
}
|
|
2714
2750
|
try {
|
|
@@ -2801,16 +2837,16 @@ var catchErrorCodes = (options, result) => {
|
|
|
2801
2837
|
);
|
|
2802
2838
|
}
|
|
2803
2839
|
};
|
|
2804
|
-
var request = (
|
|
2840
|
+
var request = (config2, options, axiosClient = import_axios.default) => {
|
|
2805
2841
|
return new CancelablePromise(async (resolve2, reject, onCancel) => {
|
|
2806
2842
|
try {
|
|
2807
|
-
const url = getUrl(
|
|
2843
|
+
const url = getUrl(config2, options);
|
|
2808
2844
|
const formData = getFormData(options);
|
|
2809
2845
|
const body = getRequestBody(options);
|
|
2810
|
-
const headers = await getHeaders(
|
|
2846
|
+
const headers = await getHeaders(config2, options);
|
|
2811
2847
|
if (!onCancel.isCancelled) {
|
|
2812
|
-
let response = await sendRequest(
|
|
2813
|
-
for (const fn of
|
|
2848
|
+
let response = await sendRequest(config2, options, url, body, formData, headers, onCancel, axiosClient);
|
|
2849
|
+
for (const fn of config2.interceptors.response._fns) {
|
|
2814
2850
|
response = await fn(response);
|
|
2815
2851
|
}
|
|
2816
2852
|
const responseBody = getResponseBody(response);
|
|
@@ -2833,8 +2869,8 @@ var request = (config, options, axiosClient = import_axios.default) => {
|
|
|
2833
2869
|
|
|
2834
2870
|
// src/generated/core/AxiosHttpRequest.ts
|
|
2835
2871
|
var AxiosHttpRequest = class extends BaseHttpRequest {
|
|
2836
|
-
constructor(
|
|
2837
|
-
super(
|
|
2872
|
+
constructor(config2) {
|
|
2873
|
+
super(config2);
|
|
2838
2874
|
}
|
|
2839
2875
|
/**
|
|
2840
2876
|
* Request method
|
|
@@ -2861,8 +2897,26 @@ var PaymentsService = class {
|
|
|
2861
2897
|
* 2. Find optimal settlement routes across chains/assets
|
|
2862
2898
|
* 3. Secure funds in escrow
|
|
2863
2899
|
* 4. Coordinate with settlement agents for execution
|
|
2864
|
-
* 5. Return a real-time result if
|
|
2865
|
-
* otherwise return a payment ID
|
|
2900
|
+
* 5. Return a real-time result if settlement completes inside the synchronous window,
|
|
2901
|
+
* otherwise return a payment ID and status for polling
|
|
2902
|
+
*
|
|
2903
|
+
* **Idempotency.** `request_id` is the idempotency key, scoped to the source account
|
|
2904
|
+
* and originator. Submitting the same `request_id` again returns the ORIGINAL payment
|
|
2905
|
+
* instead of charging a second time, so a client whose request timed out or failed
|
|
2906
|
+
* in transit can safely re-submit.
|
|
2907
|
+
*
|
|
2908
|
+
* Because a re-submission must be admissible on its own, some fields legitimately
|
|
2909
|
+
* differ between attempts and are excluded from the comparison: `quote_deadline` and
|
|
2910
|
+
* `fulfillment_deadline` (both must be in the future, so a later attempt has to move
|
|
2911
|
+
* them), and the `sender_auth` signature they are signed into.
|
|
2912
|
+
*
|
|
2913
|
+
* What must NOT differ is the payment's economics — source account and asset,
|
|
2914
|
+
* destination accounts and assets, `fulfillment_amount`, and `max_source_amount` (the
|
|
2915
|
+
* payer's own risk cap; lowering it on a retry describes a different payment, not the
|
|
2916
|
+
* same one with less headroom). Re-using one `request_id` for a genuinely different
|
|
2917
|
+
* payment is a client bug that would otherwise be silently collapsed onto the first
|
|
2918
|
+
* payment, so it is rejected with `409` and `IDEMPOTENCY_TERMS_MISMATCH`. Use a
|
|
2919
|
+
* distinct `request_id` per payment.
|
|
2866
2920
|
*
|
|
2867
2921
|
* @param data The data for the request.
|
|
2868
2922
|
* @param data.requestBody
|
|
@@ -2879,6 +2933,11 @@ var PaymentsService = class {
|
|
|
2879
2933
|
400: "Invalid request",
|
|
2880
2934
|
401: "Signature or sender authorization could not be verified",
|
|
2881
2935
|
403: "Payment declined during counterparty authorization",
|
|
2936
|
+
409: `The \`request_id\` has already been used for a payment with different economics
|
|
2937
|
+
(\`IDEMPOTENCY_TERMS_MISMATCH\`). Re-submitting one \`request_id\` is how a retry is
|
|
2938
|
+
made safe, so it must identify one payment; use a distinct \`request_id\` per
|
|
2939
|
+
payment.
|
|
2940
|
+
`,
|
|
2882
2941
|
413: "Payment request too large to process",
|
|
2883
2942
|
500: "Internal server error",
|
|
2884
2943
|
503: "A dependency is temporarily unavailable"
|
|
@@ -3166,20 +3225,20 @@ var HealthService = class {
|
|
|
3166
3225
|
|
|
3167
3226
|
// src/generated/PaymentGatewayClient.ts
|
|
3168
3227
|
var PaymentGatewayClient = class {
|
|
3169
|
-
constructor(
|
|
3228
|
+
constructor(config2, HttpRequest = AxiosHttpRequest) {
|
|
3170
3229
|
this.request = new HttpRequest({
|
|
3171
|
-
BASE:
|
|
3172
|
-
VERSION:
|
|
3173
|
-
WITH_CREDENTIALS:
|
|
3174
|
-
CREDENTIALS:
|
|
3175
|
-
TOKEN:
|
|
3176
|
-
USERNAME:
|
|
3177
|
-
PASSWORD:
|
|
3178
|
-
HEADERS:
|
|
3179
|
-
ENCODE_PATH:
|
|
3230
|
+
BASE: config2?.BASE ?? "https://payment-gw.production-mainnet.atum.xyz",
|
|
3231
|
+
VERSION: config2?.VERSION ?? "1.0.0",
|
|
3232
|
+
WITH_CREDENTIALS: config2?.WITH_CREDENTIALS ?? false,
|
|
3233
|
+
CREDENTIALS: config2?.CREDENTIALS ?? "include",
|
|
3234
|
+
TOKEN: config2?.TOKEN,
|
|
3235
|
+
USERNAME: config2?.USERNAME,
|
|
3236
|
+
PASSWORD: config2?.PASSWORD,
|
|
3237
|
+
HEADERS: config2?.HEADERS,
|
|
3238
|
+
ENCODE_PATH: config2?.ENCODE_PATH,
|
|
3180
3239
|
interceptors: {
|
|
3181
|
-
request:
|
|
3182
|
-
response:
|
|
3240
|
+
request: config2?.interceptors?.request ?? new Interceptors(),
|
|
3241
|
+
response: config2?.interceptors?.response ?? new Interceptors()
|
|
3183
3242
|
}
|
|
3184
3243
|
});
|
|
3185
3244
|
this.authorizations = new AuthorizationsService(this.request);
|
|
@@ -3389,7 +3448,8 @@ var import_payment_request_sender_auth = __toESM(require_dist4());
|
|
|
3389
3448
|
|
|
3390
3449
|
// src/index.ts
|
|
3391
3450
|
var PAYMENT_REQUEST_VERSION = "1.0";
|
|
3392
|
-
var defaultGateway = "https://payment-
|
|
3451
|
+
var defaultGateway = "https://payment-gw.production-testnet.atum.xyz";
|
|
3452
|
+
var OpenAPI2 = { ...OpenAPI, BASE: defaultGateway };
|
|
3393
3453
|
var DEFAULT_QUOTE_DEADLINE_SECONDS = 5;
|
|
3394
3454
|
var DEFAULT_FULFILLMENT_DEADLINE_SECONDS = 3600;
|
|
3395
3455
|
var DEFAULT_PAYMENT_QUOTE_DEADLINE_SECONDS = 10;
|
|
@@ -3477,9 +3537,11 @@ async function assemblePaymentRequest(params) {
|
|
|
3477
3537
|
};
|
|
3478
3538
|
}
|
|
3479
3539
|
var PaymentGatewayClient2 = class extends PaymentGatewayClient {
|
|
3480
|
-
constructor(
|
|
3481
|
-
|
|
3482
|
-
|
|
3540
|
+
constructor(config2, HttpRequest = AxiosHttpRequest) {
|
|
3541
|
+
const envBase = (typeof process !== "undefined" ? process.env.GATEWAY_URL : void 0)?.trim();
|
|
3542
|
+
const base = config2?.BASE ?? (envBase || defaultGateway);
|
|
3543
|
+
super({ ...config2, BASE: base }, HttpRequest);
|
|
3544
|
+
this.baseUrl = base;
|
|
3483
3545
|
}
|
|
3484
3546
|
/**
|
|
3485
3547
|
* Fetches chain defaults from the payment gateway.
|
|
@@ -3694,9 +3756,19 @@ var PaymentGatewayClient2 = class extends PaymentGatewayClient {
|
|
|
3694
3756
|
}
|
|
3695
3757
|
};
|
|
3696
3758
|
|
|
3759
|
+
// src/version.ts
|
|
3760
|
+
var PACKAGE_VERSION = true ? "2.2.0" : "0.0.0-dev";
|
|
3761
|
+
|
|
3762
|
+
// src/cli/load-env.ts
|
|
3763
|
+
var import_dotenv = require("dotenv");
|
|
3764
|
+
function loadCliEnv() {
|
|
3765
|
+
(0, import_dotenv.config)({ override: false, quiet: true });
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3697
3768
|
// src/cli/quote-status.ts
|
|
3769
|
+
loadCliEnv();
|
|
3698
3770
|
var program = new import_commander.Command();
|
|
3699
|
-
program.name("quote-status").description("Fetch the status of a quote-request batch (no signing required)").version(
|
|
3771
|
+
program.name("quote-status").description("Fetch the status of a quote-request batch (no signing required)").version(PACKAGE_VERSION).argument("<quoteRequestId>", "The quote_request_id (qr_...) returned by request-quote").option("--gateway <url>", "Payment gateway URL (overrides GATEWAY_URL env; default: production-testnet)").addHelpText("after", `
|
|
3700
3772
|
Example:
|
|
3701
3773
|
quote-status --gateway http://localhost:8080 qr_019f27b9-420d-7353-8263-0cf310670a77
|
|
3702
3774
|
|