@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.
@@ -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;
@@ -250,12 +256,14 @@ var require_utils = __commonJS({
250
256
  "../schemas/declarations/PaymentRequest/v1/bindings/typescript/sender-auth/dist/utils.js"(exports2) {
251
257
  "use strict";
252
258
  Object.defineProperty(exports2, "__esModule", { value: true });
253
- exports2.SOLANA_REPLAY_SLOTS = exports2.PERMIT_NONCE_DOMAIN = exports2.REPLAY_HORIZON_SECS = void 0;
259
+ exports2.REQUEST_ID_DOMAIN = exports2.SOLANA_REPLAY_SLOTS = exports2.PERMIT_NONCE_DOMAIN = exports2.REPLAY_HORIZON_SECS = void 0;
254
260
  exports2.resolvePermitDeadline = resolvePermitDeadline;
255
261
  exports2.generatePermitNonce = generatePermitNonce;
256
262
  exports2.resolvePermitNonce = resolvePermitNonce;
263
+ exports2.canonicalDepositor = canonicalDepositor;
257
264
  exports2.deterministicPermitNonce = deterministicPermitNonce2;
258
265
  exports2.deterministicSolanaNonce = deterministicSolanaNonce2;
266
+ exports2.deterministicRequestId = deterministicRequestId;
259
267
  exports2.sameAddress = sameAddress;
260
268
  exports2.assertKeyMatchesSender = assertKeyMatchesSender2;
261
269
  exports2.assertToken = assertToken;
@@ -300,6 +308,20 @@ var require_utils = __commonJS({
300
308
  const bucket = (seed >> 8n) % (1n << SOLANA_BUCKET_BITS);
301
309
  return bucket << 32n | slot;
302
310
  }
311
+ exports2.REQUEST_ID_DOMAIN = "atum-request:v1:";
312
+ function deterministicRequestId(params) {
313
+ if (!params.anchor) {
314
+ 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)");
315
+ }
316
+ if (!params.account) {
317
+ throw new Error("sender_auth: account is required to derive a deterministic request_id");
318
+ }
319
+ if (!/^[A-Za-z0-9_-]*$/.test(params.prefix)) {
320
+ throw new Error(`sender_auth: request_id prefix must contain only letters, digits, '_' or '-' (got ${JSON.stringify(params.prefix)})`);
321
+ }
322
+ const digest = (0, ethers_1.keccak256)((0, ethers_1.toUtf8Bytes)(`${exports2.REQUEST_ID_DOMAIN}${params.anchor}:${canonicalDepositor(params.account)}`));
323
+ return `${params.prefix}${digest.slice(2, 26)}`;
324
+ }
303
325
  function sameAddress(a, b) {
304
326
  if (a.startsWith("0x") && b.startsWith("0x")) {
305
327
  return a.toLowerCase() === b.toLowerCase();
@@ -891,7 +913,7 @@ var require_evm = __commonJS({
891
913
  exports2.evmAdapter = {
892
914
  async buildAuthMessage({ sourceAsset, maxSourceAmount, sourceDefaults, deadline, nonce }) {
893
915
  if (nonce === void 0) {
894
- throw new Error("sender_auth: buildAuthMessage requires an explicit nonce \u2014 derive it deterministically from a stable request id (e.g. the challenge id) so retries reproduce the same permit; pass generatePermitNonce() only for genuinely one-shot builds");
916
+ 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");
895
917
  }
896
918
  if (deadline === void 0) {
897
919
  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)");
@@ -919,6 +941,10 @@ var require_evm = __commonJS({
919
941
  throw new Error("EVM adapter sign(): signed_messages[i].message_prehash is required");
920
942
  }
921
943
  const prehash = JSON.parse(signedMessage.message_prehash);
944
+ const prehashDigest = (0, evm_escrow_encoding_1.hashPermitWitnessTransferFrom)(prehash);
945
+ if (prehashDigest.toLowerCase() !== signedMessage.message.toLowerCase()) {
946
+ throw new Error(`EVM adapter sign(): message_prehash implies digest ${prehashDigest} but message is ${signedMessage.message}`);
947
+ }
922
948
  const wallet = new ethers_1.Wallet(privateKey);
923
949
  return wallet.signTypedData(prehash.domain, prehash.types, prehash.message);
924
950
  },
@@ -1005,7 +1031,7 @@ var require_tron = __commonJS({
1005
1031
  exports2.tronAdapter = {
1006
1032
  async buildAuthMessage({ sourceAsset, maxSourceAmount, sourceDefaults, deadline, nonce }) {
1007
1033
  if (nonce === void 0) {
1008
- throw new Error("sender_auth: buildAuthMessage requires an explicit nonce \u2014 derive it deterministically from a stable request id (e.g. the challenge id) so retries reproduce the same permit; pass generatePermitNonce() only for genuinely one-shot builds");
1034
+ 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");
1009
1035
  }
1010
1036
  if (deadline === void 0) {
1011
1037
  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)");
@@ -1028,9 +1054,19 @@ var require_tron = __commonJS({
1028
1054
  const messageHash = (0, tvm_escrow_encoding_1.hashTvmPermitWitnessTransferFrom)(messagePrehash);
1029
1055
  return { messageScheme: "EVM_PERMIT2", messageHash, messagePrehash: JSON.stringify(messagePrehash) };
1030
1056
  },
1057
+ // Signs the TIP-712 typed data, not the bare digest, so a signer can inspect the fields it is
1058
+ // authorizing (Turnkey policies read `eth.eip_712.message[...]`).
1031
1059
  async sign(signedMessage, privateKey) {
1060
+ if (!signedMessage.message_prehash) {
1061
+ throw new Error("Tron adapter sign(): signed_messages[i].message_prehash is required");
1062
+ }
1063
+ const prehash = JSON.parse(signedMessage.message_prehash);
1064
+ const prehashDigest = (0, tvm_escrow_encoding_1.hashTvmPermitWitnessTransferFrom)(prehash);
1065
+ if (prehashDigest.toLowerCase() !== signedMessage.message.toLowerCase()) {
1066
+ throw new Error(`Tron adapter sign(): message_prehash implies digest ${prehashDigest} but message is ${signedMessage.message}`);
1067
+ }
1032
1068
  const wallet = new ethers_1.Wallet(privateKey.startsWith("0x") ? privateKey : "0x" + privateKey);
1033
- return wallet.signingKey.sign(signedMessage.message).serialized;
1069
+ return wallet.signTypedData(prehash.domain, prehash.types, prehash.message);
1034
1070
  },
1035
1071
  recoverSigner(signed, source) {
1036
1072
  let recovered;
@@ -2687,6 +2723,7 @@ var require_client = __commonJS({
2687
2723
  retryCounter;
2688
2724
  backoffHistogram;
2689
2725
  activityPendingCounter;
2726
+ signDurationHistogram;
2690
2727
  // The private API scalar is held only long enough to construct the stamper. After
2691
2728
  // `getStamper()` runs, this is nulled out so the hex string doesn't survive on the heap
2692
2729
  // for the client's lifetime. Web Crypto holds the imported key as an opaque CryptoKey.
@@ -2715,6 +2752,10 @@ var require_client = __commonJS({
2715
2752
  this.activityPendingCounter = meter.createCounter("turnkey.activity_pending_total", {
2716
2753
  description: "Activities that entered async polling (did not complete synchronously in the submit response), labeled by activity type"
2717
2754
  });
2755
+ this.signDurationHistogram = meter.createHistogram("turnkey.sign_duration_ms", {
2756
+ description: "Full Turnkey activity round-trip latency (concurrency-semaphore wait + submit + poll), labeled by activity type and outcome (success|error)",
2757
+ unit: "ms"
2758
+ });
2718
2759
  }
2719
2760
  emitRetry(reason, path) {
2720
2761
  try {
@@ -2734,6 +2775,12 @@ var require_client = __commonJS({
2734
2775
  } catch {
2735
2776
  }
2736
2777
  }
2778
+ emitSignDuration(durationMs, activityType, outcome) {
2779
+ try {
2780
+ this.signDurationHistogram.record(durationMs, { activity_type: activityType, outcome });
2781
+ } catch {
2782
+ }
2783
+ }
2737
2784
  // ── public surface ─────────────────────────────────────────────────────────
2738
2785
  /** Boot-time reachability + auth probe. Throws TURNKEY_AUTH on bad credentials. */
2739
2786
  async whoami() {
@@ -2783,39 +2830,46 @@ var require_client = __commonJS({
2783
2830
  }
2784
2831
  // ── activity submission ────────────────────────────────────────────────────
2785
2832
  async submitActivity(activityType, endpoint, parameters) {
2786
- const body = {
2787
- type: activityType,
2788
- timestampMs: String(Date.now()),
2789
- organizationId: this.opts.organizationId,
2790
- parameters
2791
- };
2792
- const signWith = typeof parameters.signWith === "string" ? parameters.signWith : void 0;
2793
- const ctx = `activity=${activityType}${signWith ? ` signWith=${signWith}` : ""}`;
2794
- const resp = await this.queryPost(`/public/v1/submit/${endpoint}`, body);
2795
- let activity = resp.activity;
2796
- const deadline = Date.now() + POLL_TIMEOUT_MS;
2797
- let pollAttempt = 0;
2798
- if (activity?.status === "ACTIVITY_STATUS_PENDING") {
2799
- this.emitActivityPending(activityType);
2800
- }
2801
- while (activity?.status === "ACTIVITY_STATUS_PENDING") {
2802
- if (Date.now() > deadline) {
2803
- throw (0, km_core_1.signerRetryable)(km_core_1.SignerErrorCode.TURNKEY_SIGNER_UNAVAILABLE, `Turnkey ${ctx} activityId=${activity.id} stayed PENDING past ${POLL_TIMEOUT_MS}ms`);
2833
+ const start = Date.now();
2834
+ let outcome = "error";
2835
+ try {
2836
+ const body = {
2837
+ type: activityType,
2838
+ timestampMs: String(Date.now()),
2839
+ organizationId: this.opts.organizationId,
2840
+ parameters
2841
+ };
2842
+ const signWith = typeof parameters.signWith === "string" ? parameters.signWith : void 0;
2843
+ const ctx = `activity=${activityType}${signWith ? ` signWith=${signWith}` : ""}`;
2844
+ const resp = await this.queryPost(`/public/v1/submit/${endpoint}`, body);
2845
+ let activity = resp.activity;
2846
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
2847
+ let pollAttempt = 0;
2848
+ if (activity?.status === "ACTIVITY_STATUS_PENDING") {
2849
+ this.emitActivityPending(activityType);
2804
2850
  }
2805
- const sleepIdx = Math.min(pollAttempt, POLL_BACKOFF_MS.length - 1);
2806
- await (0, promises_1.setTimeout)(POLL_BACKOFF_MS[sleepIdx]);
2807
- pollAttempt++;
2808
- const polled = await this.queryPost("/public/v1/query/get_activity", { organizationId: this.opts.organizationId, activityId: activity.id });
2809
- activity = polled.activity;
2810
- }
2811
- if (activity?.status !== "ACTIVITY_STATUS_COMPLETED") {
2812
- const failure = activity?.failure ?? {};
2813
- throw (0, km_core_1.signerTerminal)(km_core_1.SignerErrorCode.TURNKEY_ACTIVITY_FAILED, `Turnkey ${ctx} activityId=${activity?.id ?? "?"} status=${activity?.status ?? "unknown"}; ${JSON.stringify(failure)}`);
2814
- }
2815
- if (activity.result == null) {
2816
- throw (0, km_core_1.signerTerminal)(km_core_1.SignerErrorCode.TURNKEY_BAD_RESPONSE, `Turnkey ${ctx} activityId=${activity.id} COMPLETED but result was missing`);
2851
+ while (activity?.status === "ACTIVITY_STATUS_PENDING") {
2852
+ if (Date.now() > deadline) {
2853
+ throw (0, km_core_1.signerRetryable)(km_core_1.SignerErrorCode.TURNKEY_SIGNER_UNAVAILABLE, `Turnkey ${ctx} activityId=${activity.id} stayed PENDING past ${POLL_TIMEOUT_MS}ms`);
2854
+ }
2855
+ const sleepIdx = Math.min(pollAttempt, POLL_BACKOFF_MS.length - 1);
2856
+ await (0, promises_1.setTimeout)(POLL_BACKOFF_MS[sleepIdx]);
2857
+ pollAttempt++;
2858
+ const polled = await this.queryPost("/public/v1/query/get_activity", { organizationId: this.opts.organizationId, activityId: activity.id });
2859
+ activity = polled.activity;
2860
+ }
2861
+ if (activity?.status !== "ACTIVITY_STATUS_COMPLETED") {
2862
+ const failure = activity?.failure ?? {};
2863
+ throw (0, km_core_1.signerTerminal)(km_core_1.SignerErrorCode.TURNKEY_ACTIVITY_FAILED, `Turnkey ${ctx} activityId=${activity?.id ?? "?"} status=${activity?.status ?? "unknown"}; ${JSON.stringify(failure)}`);
2864
+ }
2865
+ if (activity.result == null) {
2866
+ throw (0, km_core_1.signerTerminal)(km_core_1.SignerErrorCode.TURNKEY_BAD_RESPONSE, `Turnkey ${ctx} activityId=${activity.id} COMPLETED but result was missing`);
2867
+ }
2868
+ outcome = "success";
2869
+ return activity.result;
2870
+ } finally {
2871
+ this.emitSignDuration(Date.now() - start, activityType, outcome);
2817
2872
  }
2818
- return activity.result;
2819
2873
  }
2820
2874
  // ── HTTP layer ────────────────────────────────────────────────────────────
2821
2875
  /**
@@ -3089,17 +3143,33 @@ var require_secp256k1_helpers = __commonJS({
3089
3143
  "use strict";
3090
3144
  Object.defineProperty(exports2, "__esModule", { value: true });
3091
3145
  exports2.signSecp256k1PreimageAndVerify = signSecp256k1PreimageAndVerify;
3146
+ exports2.signSecp256k1TypedDataAndVerify = signSecp256k1TypedDataAndVerify;
3092
3147
  exports2.bytesFromHexOrBytes = bytesFromHexOrBytes;
3093
3148
  var ethers_1 = require("ethers");
3094
3149
  var km_core_1 = require_dist5();
3150
+ var EIP712_HASH_FUNCTION = "HASH_FUNCTION_NOT_APPLICABLE";
3095
3151
  async function signSecp256k1PreimageAndVerify(client, signWith, preimage, expectedEvmAddrLower) {
3096
3152
  const preimageHex = (0, ethers_1.hexlify)(preimage).slice(2);
3097
- const { r, s, v } = await client.signRawPayload({
3153
+ const parts = await client.signRawPayload({
3098
3154
  signWith,
3099
3155
  payload: preimageHex,
3100
3156
  encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
3101
3157
  hashFunction: "HASH_FUNCTION_KECCAK256"
3102
3158
  });
3159
+ return recoverAndVerify(parts, (0, ethers_1.keccak256)(preimage), expectedEvmAddrLower);
3160
+ }
3161
+ async function signSecp256k1TypedDataAndVerify(client, signWith, domain, types, message, expectedEvmAddrLower) {
3162
+ const payload = ethers_1.TypedDataEncoder.getPayload(domain, types, message);
3163
+ const parts = await client.signRawPayload({
3164
+ signWith,
3165
+ payload: JSON.stringify(payload),
3166
+ encoding: "PAYLOAD_ENCODING_EIP712",
3167
+ hashFunction: EIP712_HASH_FUNCTION
3168
+ });
3169
+ const digestHex = ethers_1.TypedDataEncoder.hash(domain, types, message);
3170
+ return recoverAndVerify(parts, digestHex, expectedEvmAddrLower);
3171
+ }
3172
+ function recoverAndVerify({ r, s, v }, digestHex, expectedEvmAddrLower) {
3103
3173
  let sig;
3104
3174
  try {
3105
3175
  sig = ethers_1.Signature.from({
@@ -3110,7 +3180,6 @@ var require_secp256k1_helpers = __commonJS({
3110
3180
  } catch (err) {
3111
3181
  throw (0, km_core_1.signerTerminal)(km_core_1.SignerErrorCode.TURNKEY_BAD_RESPONSE, `Turnkey returned malformed secp256k1 signature parts r=${r} s=${s} v=${v}`, err instanceof Error ? err : void 0);
3112
3182
  }
3113
- const digestHex = (0, ethers_1.keccak256)(preimage);
3114
3183
  const recovered = (0, ethers_1.recoverAddress)(digestHex, sig).toLowerCase();
3115
3184
  if (recovered !== expectedEvmAddrLower) {
3116
3185
  throw (0, km_core_1.signerTerminal)(km_core_1.SignerErrorCode.TURNKEY_BAD_RESPONSE, `secp256k1 sign-then-recover mismatch: recovered ${recovered}, expected ${expectedEvmAddrLower}`);
@@ -3174,8 +3243,7 @@ ${msgBytes.length}`);
3174
3243
  }
3175
3244
  async signTypedData(domain, types, message) {
3176
3245
  const { signWith, lower } = this.requireAddresses();
3177
- const preimage = (0, ethers_1.getBytes)(ethers_1.TypedDataEncoder.encode(domain, types, message));
3178
- return (0, secp256k1_helpers_1.signSecp256k1PreimageAndVerify)(this.client, signWith, preimage, lower);
3246
+ return (0, secp256k1_helpers_1.signSecp256k1TypedDataAndVerify)(this.client, signWith, domain, types, message, lower);
3179
3247
  }
3180
3248
  async signTransaction(tx) {
3181
3249
  const { signWith, lower } = this.requireAddresses();
@@ -3292,6 +3360,7 @@ var require_solana2 = __commonJS({
3292
3360
  };
3293
3361
  Object.defineProperty(exports2, "__esModule", { value: true });
3294
3362
  exports2.TurnkeySolanaSigner = void 0;
3363
+ exports2.encodePayload = encodePayload;
3295
3364
  var web3_js_1 = require("@solana/web3.js");
3296
3365
  var bs58_1 = __importDefault(require("bs58"));
3297
3366
  var km_core_1 = require_dist5();
@@ -3325,8 +3394,7 @@ var require_solana2 = __commonJS({
3325
3394
  }
3326
3395
  const { r, s } = await this.client.signRawPayload({
3327
3396
  signWith: this.address,
3328
- payload: Buffer.from(bytes).toString("hex"),
3329
- encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
3397
+ ...encodePayload(bytes),
3330
3398
  hashFunction: "HASH_FUNCTION_NOT_APPLICABLE"
3331
3399
  });
3332
3400
  const sig = Buffer.concat([Buffer.from(stripHex(r), "hex"), Buffer.from(stripHex(s), "hex")]);
@@ -3367,6 +3435,15 @@ var require_solana2 = __commonJS({
3367
3435
  }
3368
3436
  };
3369
3437
  exports2.TurnkeySolanaSigner = TurnkeySolanaSigner2;
3438
+ function encodePayload(bytes) {
3439
+ const buf = Buffer.from(bytes);
3440
+ const text = buf.toString("utf8");
3441
+ const textual = buf.every((b) => b >= 32 && b <= 126 || b === 9 || b === 10 || b === 13);
3442
+ if (textual && Buffer.from(text, "utf8").equals(buf)) {
3443
+ return { payload: text, encoding: "PAYLOAD_ENCODING_TEXT_UTF8" };
3444
+ }
3445
+ return { payload: buf.toString("hex"), encoding: "PAYLOAD_ENCODING_HEXADECIMAL" };
3446
+ }
3370
3447
  function stripHex(s) {
3371
3448
  return s.startsWith("0x") ? s.slice(2) : s;
3372
3449
  }
@@ -3434,8 +3511,7 @@ ${messageBytes.length}`);
3434
3511
  }
3435
3512
  async signTypedData(domain, types, message) {
3436
3513
  const { address, evmAddr } = this.requireReady();
3437
- const preimage = (0, ethers_1.getBytes)(ethers_1.TypedDataEncoder.encode(domain, types, message));
3438
- return (0, secp256k1_helpers_1.signSecp256k1PreimageAndVerify)(this.client, address, preimage, evmAddr);
3514
+ return (0, secp256k1_helpers_1.signSecp256k1TypedDataAndVerify)(this.client, address, domain, types, message, evmAddr);
3439
3515
  }
3440
3516
  async signTransaction(tx) {
3441
3517
  const { address, evmAddr } = this.requireReady();
@@ -3814,7 +3890,7 @@ var require_policies = __commonJS({
3814
3890
  policyName: "allowSignRawPayload",
3815
3891
  effect: "EFFECT_ALLOW",
3816
3892
  condition: `activity.type in ['ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2', 'ACTIVITY_TYPE_SIGN_RAW_PAYLOADS'] && activity.params.hash_function in ['HASH_FUNCTION_KECCAK256', 'HASH_FUNCTION_NOT_APPLICABLE']`,
3817
- notes: "Allows signRawPayload with the hash functions the agent runtime uses."
3893
+ notes: "Allows signRawPayload with the hash functions the runtime uses: KECCAK256 (EIP-191/TIP-191/W3C proofs) and NOT_APPLICABLE (Solana ed25519 + EIP-712 typed data)."
3818
3894
  };
3819
3895
  }
3820
3896
  function buildDenyApproveToNonAllowedSpenderEvm(args) {
@@ -4459,7 +4535,7 @@ var require_dist6 = __commonJS({
4459
4535
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
4460
4536
  };
4461
4537
  Object.defineProperty(exports2, "__esModule", { value: true });
4462
- exports2.bytesFromHexOrBytes = exports2.signSecp256k1PreimageAndVerify = exports2.extractTronSignatureFromTransactionProtobuf = exports2.TurnkeyTronSigner = exports2.TurnkeySolanaSigner = exports2.TurnkeyEvmSigner = exports2.noopMeter = exports2.noopLogger = exports2.TURNKEY_RETRY_BACKOFF_MS = exports2.resolveTurnkeyAccountAddress = exports2.p1363ToDer = exports2.createTurnkeyStamper = exports2.createIsolatedTurnkeyClient = exports2.getOrCreateTurnkeyClient = exports2.TurnkeyClient = void 0;
4538
+ exports2.bytesFromHexOrBytes = exports2.signSecp256k1PreimageAndVerify = exports2.extractTronSignatureFromTransactionProtobuf = exports2.TurnkeyTronSigner = exports2.encodePayload = exports2.TurnkeySolanaSigner = exports2.TurnkeyEvmSigner = exports2.noopMeter = exports2.noopLogger = exports2.TURNKEY_RETRY_BACKOFF_MS = exports2.resolveTurnkeyAccountAddress = exports2.p1363ToDer = exports2.createTurnkeyStamper = exports2.createIsolatedTurnkeyClient = exports2.getOrCreateTurnkeyClient = exports2.TurnkeyClient = void 0;
4463
4539
  var client_1 = require_client();
4464
4540
  Object.defineProperty(exports2, "TurnkeyClient", { enumerable: true, get: function() {
4465
4541
  return client_1.TurnkeyClient;
@@ -4502,6 +4578,9 @@ var require_dist6 = __commonJS({
4502
4578
  Object.defineProperty(exports2, "TurnkeySolanaSigner", { enumerable: true, get: function() {
4503
4579
  return solana_1.TurnkeySolanaSigner;
4504
4580
  } });
4581
+ Object.defineProperty(exports2, "encodePayload", { enumerable: true, get: function() {
4582
+ return solana_1.encodePayload;
4583
+ } });
4505
4584
  var tron_1 = require_tron2();
4506
4585
  Object.defineProperty(exports2, "TurnkeyTronSigner", { enumerable: true, get: function() {
4507
4586
  return tron_1.TurnkeyTronSigner;
@@ -4563,13 +4642,13 @@ async function signWithTurnkeyClient(client, chainPrefix, cfg, message, messageP
4563
4642
  };
4564
4643
  switch (chainPrefix) {
4565
4644
  case "eip155": {
4566
- const { domain, types, message: typedMessage } = parsePrehash(messagePrehash, "EVM");
4645
+ const { domain, types, message: typedMessage } = parsePrehash(messagePrehash, "EVM", message);
4567
4646
  const signer = new import_turnkey_signer.TurnkeyEvmSigner(client, deps);
4568
4647
  await signer.initialize();
4569
4648
  return { signature: await signer.signTypedData(domain, types, typedMessage) };
4570
4649
  }
4571
4650
  case "tron": {
4572
- const { domain, types, message: typedMessage } = parsePrehash(messagePrehash, "Tron");
4651
+ const { domain, types, message: typedMessage } = parsePrehash(messagePrehash, "Tron", message);
4573
4652
  const signer = new import_turnkey_signer.TurnkeyTronSigner(client, deps);
4574
4653
  await signer.initialize();
4575
4654
  return { signature: await signer.signTypedData(domain, types, typedMessage) };
@@ -4597,7 +4676,7 @@ function decodeSolanaMessageHex(message) {
4597
4676
  }
4598
4677
  return Uint8Array.from(Buffer.from(hex, "hex"));
4599
4678
  }
4600
- function parsePrehash(messagePrehash, label) {
4679
+ function parsePrehash(messagePrehash, label, advertisedDigest) {
4601
4680
  if (!messagePrehash) {
4602
4681
  throw new Error(`Turnkey ${label} signing requires signed_messages[0].message_prehash`);
4603
4682
  }
@@ -4611,13 +4690,21 @@ function parsePrehash(messagePrehash, label) {
4611
4690
  if (!isObject(parsed) || !isObject(parsed.domain) || !isObject(parsed.types) || !isObject(parsed.message)) {
4612
4691
  throw new Error(`Turnkey ${label} signing: message_prehash must be a JSON object with domain, types and message`);
4613
4692
  }
4614
- return { domain: parsed.domain, types: parsed.types, message: parsed.message };
4693
+ const { EIP712Domain: _derived, ...structTypes } = parsed.types;
4694
+ const prehashDigest = import_ethers5.TypedDataEncoder.hash(parsed.domain, structTypes, parsed.message);
4695
+ if (prehashDigest.toLowerCase() !== advertisedDigest.toLowerCase()) {
4696
+ throw new Error(
4697
+ `Turnkey ${label} signing: message_prehash implies digest ${prehashDigest} but message is ${advertisedDigest}`
4698
+ );
4699
+ }
4700
+ return { domain: parsed.domain, types: structTypes, message: parsed.message };
4615
4701
  }
4616
- var import_turnkey_signer, TURNKEY_API_URL, DEFAULTS;
4702
+ var import_turnkey_signer, import_ethers5, TURNKEY_API_URL, DEFAULTS;
4617
4703
  var init_turnkey_impl = __esm({
4618
4704
  "src/signers/turnkey-impl.ts"() {
4619
4705
  "use strict";
4620
4706
  import_turnkey_signer = __toESM(require_dist6());
4707
+ import_ethers5 = require("ethers");
4621
4708
  TURNKEY_API_URL = "https://api.turnkey.com";
4622
4709
  DEFAULTS = {
4623
4710
  concurrencyLimit: 10,
@@ -4646,7 +4733,7 @@ var Interceptors = class {
4646
4733
  }
4647
4734
  };
4648
4735
  var OpenAPI = {
4649
- BASE: "https://api.atum.network",
4736
+ BASE: "https://payment-gw.production-mainnet.atum.xyz",
4650
4737
  CREDENTIALS: "include",
4651
4738
  ENCODE_PATH: void 0,
4652
4739
  HEADERS: void 0,
@@ -4663,8 +4750,8 @@ var OpenAPI = {
4663
4750
 
4664
4751
  // src/generated/core/BaseHttpRequest.ts
4665
4752
  var BaseHttpRequest = class {
4666
- constructor(config) {
4667
- this.config = config;
4753
+ constructor(config2) {
4754
+ this.config = config2;
4668
4755
  }
4669
4756
  };
4670
4757
 
@@ -4815,15 +4902,15 @@ var getQueryString = (params) => {
4815
4902
  Object.entries(params).forEach(([key, value]) => encodePair(key, value));
4816
4903
  return qs.length ? `?${qs.join("&")}` : "";
4817
4904
  };
4818
- var getUrl = (config, options) => {
4819
- const encoder = config.ENCODE_PATH || encodeURI;
4820
- const path = options.url.replace("{api-version}", config.VERSION).replace(/{(.*?)}/g, (substring, group) => {
4905
+ var getUrl = (config2, options) => {
4906
+ const encoder = config2.ENCODE_PATH || encodeURI;
4907
+ const path = options.url.replace("{api-version}", config2.VERSION).replace(/{(.*?)}/g, (substring, group) => {
4821
4908
  if (options.path?.hasOwnProperty(group)) {
4822
4909
  return encoder(String(options.path[group]));
4823
4910
  }
4824
4911
  return substring;
4825
4912
  });
4826
- const url = config.BASE + path;
4913
+ const url = config2.BASE + path;
4827
4914
  return options.query ? url + getQueryString(options.query) : url;
4828
4915
  };
4829
4916
  var getFormData = (options) => {
@@ -4853,12 +4940,12 @@ var resolve = async (options, resolver) => {
4853
4940
  }
4854
4941
  return resolver;
4855
4942
  };
4856
- var getHeaders = async (config, options) => {
4943
+ var getHeaders = async (config2, options) => {
4857
4944
  const [token, username, password, additionalHeaders] = await Promise.all([
4858
- resolve(options, config.TOKEN),
4859
- resolve(options, config.USERNAME),
4860
- resolve(options, config.PASSWORD),
4861
- resolve(options, config.HEADERS)
4945
+ resolve(options, config2.TOKEN),
4946
+ resolve(options, config2.USERNAME),
4947
+ resolve(options, config2.PASSWORD),
4948
+ resolve(options, config2.HEADERS)
4862
4949
  ]);
4863
4950
  const headers = Object.entries({
4864
4951
  Accept: "application/json",
@@ -4898,7 +4985,7 @@ var getRequestBody = (options) => {
4898
4985
  }
4899
4986
  return void 0;
4900
4987
  };
4901
- var sendRequest = async (config, options, url, body, formData, headers, onCancel, axiosClient) => {
4988
+ var sendRequest = async (config2, options, url, body, formData, headers, onCancel, axiosClient) => {
4902
4989
  const controller = new AbortController();
4903
4990
  let requestConfig = {
4904
4991
  data: body ?? formData,
@@ -4906,10 +4993,10 @@ var sendRequest = async (config, options, url, body, formData, headers, onCancel
4906
4993
  method: options.method,
4907
4994
  signal: controller.signal,
4908
4995
  url,
4909
- withCredentials: config.WITH_CREDENTIALS
4996
+ withCredentials: config2.WITH_CREDENTIALS
4910
4997
  };
4911
4998
  onCancel(() => controller.abort());
4912
- for (const fn of config.interceptors.request._fns) {
4999
+ for (const fn of config2.interceptors.request._fns) {
4913
5000
  requestConfig = await fn(requestConfig);
4914
5001
  }
4915
5002
  try {
@@ -5002,16 +5089,16 @@ var catchErrorCodes = (options, result) => {
5002
5089
  );
5003
5090
  }
5004
5091
  };
5005
- var request = (config, options, axiosClient = import_axios.default) => {
5092
+ var request = (config2, options, axiosClient = import_axios.default) => {
5006
5093
  return new CancelablePromise(async (resolve2, reject, onCancel) => {
5007
5094
  try {
5008
- const url = getUrl(config, options);
5095
+ const url = getUrl(config2, options);
5009
5096
  const formData = getFormData(options);
5010
5097
  const body = getRequestBody(options);
5011
- const headers = await getHeaders(config, options);
5098
+ const headers = await getHeaders(config2, options);
5012
5099
  if (!onCancel.isCancelled) {
5013
- let response = await sendRequest(config, options, url, body, formData, headers, onCancel, axiosClient);
5014
- for (const fn of config.interceptors.response._fns) {
5100
+ let response = await sendRequest(config2, options, url, body, formData, headers, onCancel, axiosClient);
5101
+ for (const fn of config2.interceptors.response._fns) {
5015
5102
  response = await fn(response);
5016
5103
  }
5017
5104
  const responseBody = getResponseBody(response);
@@ -5034,8 +5121,8 @@ var request = (config, options, axiosClient = import_axios.default) => {
5034
5121
 
5035
5122
  // src/generated/core/AxiosHttpRequest.ts
5036
5123
  var AxiosHttpRequest = class extends BaseHttpRequest {
5037
- constructor(config) {
5038
- super(config);
5124
+ constructor(config2) {
5125
+ super(config2);
5039
5126
  }
5040
5127
  /**
5041
5128
  * Request method
@@ -5062,8 +5149,26 @@ var PaymentsService = class {
5062
5149
  * 2. Find optimal settlement routes across chains/assets
5063
5150
  * 3. Secure funds in escrow
5064
5151
  * 4. Coordinate with settlement agents for execution
5065
- * 5. Return a real-time result if processing completes within 30 seconds,
5066
- * otherwise return a payment ID for status polling
5152
+ * 5. Return a real-time result if settlement completes inside the synchronous window,
5153
+ * otherwise return a payment ID and status for polling
5154
+ *
5155
+ * **Idempotency.** `request_id` is the idempotency key, scoped to the source account
5156
+ * and originator. Submitting the same `request_id` again returns the ORIGINAL payment
5157
+ * instead of charging a second time, so a client whose request timed out or failed
5158
+ * in transit can safely re-submit.
5159
+ *
5160
+ * Because a re-submission must be admissible on its own, some fields legitimately
5161
+ * differ between attempts and are excluded from the comparison: `quote_deadline` and
5162
+ * `fulfillment_deadline` (both must be in the future, so a later attempt has to move
5163
+ * them), and the `sender_auth` signature they are signed into.
5164
+ *
5165
+ * What must NOT differ is the payment's economics — source account and asset,
5166
+ * destination accounts and assets, `fulfillment_amount`, and `max_source_amount` (the
5167
+ * payer's own risk cap; lowering it on a retry describes a different payment, not the
5168
+ * same one with less headroom). Re-using one `request_id` for a genuinely different
5169
+ * payment is a client bug that would otherwise be silently collapsed onto the first
5170
+ * payment, so it is rejected with `409` and `IDEMPOTENCY_TERMS_MISMATCH`. Use a
5171
+ * distinct `request_id` per payment.
5067
5172
  *
5068
5173
  * @param data The data for the request.
5069
5174
  * @param data.requestBody
@@ -5080,6 +5185,11 @@ var PaymentsService = class {
5080
5185
  400: "Invalid request",
5081
5186
  401: "Signature or sender authorization could not be verified",
5082
5187
  403: "Payment declined during counterparty authorization",
5188
+ 409: `The \`request_id\` has already been used for a payment with different economics
5189
+ (\`IDEMPOTENCY_TERMS_MISMATCH\`). Re-submitting one \`request_id\` is how a retry is
5190
+ made safe, so it must identify one payment; use a distinct \`request_id\` per
5191
+ payment.
5192
+ `,
5083
5193
  413: "Payment request too large to process",
5084
5194
  500: "Internal server error",
5085
5195
  503: "A dependency is temporarily unavailable"
@@ -5367,20 +5477,20 @@ var HealthService = class {
5367
5477
 
5368
5478
  // src/generated/PaymentGatewayClient.ts
5369
5479
  var PaymentGatewayClient = class {
5370
- constructor(config, HttpRequest = AxiosHttpRequest) {
5480
+ constructor(config2, HttpRequest = AxiosHttpRequest) {
5371
5481
  this.request = new HttpRequest({
5372
- BASE: config?.BASE ?? "https://api.atum.network",
5373
- VERSION: config?.VERSION ?? "1.0.0",
5374
- WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false,
5375
- CREDENTIALS: config?.CREDENTIALS ?? "include",
5376
- TOKEN: config?.TOKEN,
5377
- USERNAME: config?.USERNAME,
5378
- PASSWORD: config?.PASSWORD,
5379
- HEADERS: config?.HEADERS,
5380
- ENCODE_PATH: config?.ENCODE_PATH,
5482
+ BASE: config2?.BASE ?? "https://payment-gw.production-mainnet.atum.xyz",
5483
+ VERSION: config2?.VERSION ?? "1.0.0",
5484
+ WITH_CREDENTIALS: config2?.WITH_CREDENTIALS ?? false,
5485
+ CREDENTIALS: config2?.CREDENTIALS ?? "include",
5486
+ TOKEN: config2?.TOKEN,
5487
+ USERNAME: config2?.USERNAME,
5488
+ PASSWORD: config2?.PASSWORD,
5489
+ HEADERS: config2?.HEADERS,
5490
+ ENCODE_PATH: config2?.ENCODE_PATH,
5381
5491
  interceptors: {
5382
- request: config?.interceptors?.request ?? new Interceptors(),
5383
- response: config?.interceptors?.response ?? new Interceptors()
5492
+ request: config2?.interceptors?.request ?? new Interceptors(),
5493
+ response: config2?.interceptors?.response ?? new Interceptors()
5384
5494
  }
5385
5495
  });
5386
5496
  this.authorizations = new AuthorizationsService(this.request);
@@ -5647,7 +5757,8 @@ function createSenderSigner(sourceChainId, opts) {
5647
5757
 
5648
5758
  // src/index.ts
5649
5759
  var PAYMENT_REQUEST_VERSION = "1.0";
5650
- var defaultGateway = "https://payment-gateway-testnet.atumlabs.xyz";
5760
+ var defaultGateway = "https://payment-gw.production-testnet.atum.xyz";
5761
+ var OpenAPI2 = { ...OpenAPI, BASE: defaultGateway };
5651
5762
  var DEFAULT_QUOTE_DEADLINE_SECONDS = 5;
5652
5763
  var DEFAULT_FULFILLMENT_DEADLINE_SECONDS = 3600;
5653
5764
  var DEFAULT_PAYMENT_QUOTE_DEADLINE_SECONDS = 10;
@@ -5735,9 +5846,11 @@ async function assemblePaymentRequest(params) {
5735
5846
  };
5736
5847
  }
5737
5848
  var PaymentGatewayClient2 = class extends PaymentGatewayClient {
5738
- constructor(config, HttpRequest = AxiosHttpRequest) {
5739
- super(config, HttpRequest);
5740
- this.baseUrl = config?.BASE ?? defaultGateway;
5849
+ constructor(config2, HttpRequest = AxiosHttpRequest) {
5850
+ const envBase = (typeof process !== "undefined" ? process.env.GATEWAY_URL : void 0)?.trim();
5851
+ const base = config2?.BASE ?? (envBase || defaultGateway);
5852
+ super({ ...config2, BASE: base }, HttpRequest);
5853
+ this.baseUrl = base;
5741
5854
  }
5742
5855
  /**
5743
5856
  * Fetches chain defaults from the payment gateway.
@@ -5962,8 +6075,87 @@ function parseSeconds(optionName) {
5962
6075
  };
5963
6076
  }
5964
6077
 
6078
+ // src/version.ts
6079
+ var PACKAGE_VERSION = true ? "2.2.0" : "0.0.0-dev";
6080
+
6081
+ // src/cli/payment-outcome.ts
6082
+ var EXIT_COMPLETED = 0;
6083
+ var EXIT_ERROR = 1;
6084
+ var EXIT_PENDING = 2;
6085
+ var EXIT_TERMINAL_FAILURE = 3;
6086
+ function isTerminalStatus(status) {
6087
+ return status === "completed" || status === "failed" || status === "cancelled";
6088
+ }
6089
+ function describePaymentOutcome(input) {
6090
+ const { status, paymentId, requestId, idempotentReplay, errorCode } = input;
6091
+ const payment = paymentId ? `payment ${paymentId}` : "the payment";
6092
+ const replay = idempotentReplay ? " This is the payment your earlier attempt created \u2014 nothing additional was charged." : "";
6093
+ if (status === "completed") {
6094
+ return {
6095
+ exitCode: EXIT_COMPLETED,
6096
+ summary: `completed: ${payment} settled.${replay}`
6097
+ };
6098
+ }
6099
+ if (status === "failed" || status === "cancelled") {
6100
+ const reason = errorCode ? ` (${errorCode})` : "";
6101
+ return {
6102
+ exitCode: EXIT_TERMINAL_FAILURE,
6103
+ summary: `${status}: ${payment} ended without settling${reason}. This is final \u2014 re-submitting ${requestId ? `request id ${requestId}` : "the same request id"} returns this same failed payment, so a fresh attempt needs a NEW request id.`
6104
+ };
6105
+ }
6106
+ const resubmit = requestId ? `request id ${requestId}` : "the same request id";
6107
+ return {
6108
+ exitCode: EXIT_PENDING,
6109
+ summary: `pending: ${payment} was accepted and is still settling.${replay} To collect the result, keep checking its status, or re-submit ${resubmit}. Do not start a new payment under a different request id \u2014 that would pay twice.`
6110
+ };
6111
+ }
6112
+
6113
+ // src/cli/wait-for-terminal.ts
6114
+ var DEFAULT_WAIT_SECONDS = 60;
6115
+ var DEFAULT_POLL_INTERVAL_MS = 2e3;
6116
+ async function waitForTerminalStatus(params) {
6117
+ const {
6118
+ fetchStatus,
6119
+ budgetMs,
6120
+ intervalMs = DEFAULT_POLL_INTERVAL_MS,
6121
+ now = () => Date.now(),
6122
+ sleep = defaultSleep,
6123
+ onError
6124
+ } = params;
6125
+ const deadline = now() + budgetMs;
6126
+ let snapshot;
6127
+ let errorCount = 0;
6128
+ while (now() < deadline) {
6129
+ const remaining = deadline - now();
6130
+ await sleep(Math.min(intervalMs, remaining));
6131
+ try {
6132
+ snapshot = await fetchStatus();
6133
+ } catch (error) {
6134
+ errorCount += 1;
6135
+ onError?.(error);
6136
+ continue;
6137
+ }
6138
+ if (isTerminalStatus(snapshot.status)) {
6139
+ return { snapshot, timedOut: false, errorCount };
6140
+ }
6141
+ }
6142
+ return { snapshot, timedOut: true, errorCount };
6143
+ }
6144
+ function defaultSleep(ms) {
6145
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
6146
+ }
6147
+
5965
6148
  // src/cli/send-payment.ts
5966
6149
  var readline = __toESM(require("readline"));
6150
+
6151
+ // src/cli/load-env.ts
6152
+ var import_dotenv = require("dotenv");
6153
+ function loadCliEnv() {
6154
+ (0, import_dotenv.config)({ override: false, quiet: true });
6155
+ }
6156
+
6157
+ // src/cli/send-payment.ts
6158
+ loadCliEnv();
5967
6159
  function collect(value, previous) {
5968
6160
  return previous.concat([value]);
5969
6161
  }
@@ -5983,7 +6175,7 @@ function loadTurnkeyConfigFromEnv() {
5983
6175
  };
5984
6176
  }
5985
6177
  var program = new import_commander.Command();
5986
- program.name("send-payment").description("Send a cross-chain payment through Atum Payment Gateway").version("1.0.0").argument("<sender>", "Depositor address (e.g., 0xF0D0...)").argument("<amount>", "Amount to send (in smallest units, e.g., 100000 for 0.10 USDC)").argument("<sourceAsset>", "Source asset in CAIP-19 format").argument("<receiver>", "Destination account address").argument("<destinationAsset>", "Destination asset in CAIP-19 format").option("--gateway <url>", "Payment gateway URL", defaultGateway).option("--request-id <id>", "Idempotency key; seeds the on-chain deposit nonce. Reuse the same id to retry a payment safely. Omit to auto-generate a one-shot id.").option("--request-id-prefix <prefix>", "Custom request ID prefix (default: pmt-gw-cli)").option("--quote-deadline-seconds <n>", `Quote collection window in seconds (default: ${DEFAULT_PAYMENT_QUOTE_DEADLINE_SECONDS})`, parseSeconds("--quote-deadline-seconds")).option("--fulfillment-deadline-seconds <n>", `Settlement horizon in seconds (default: ${DEFAULT_FULFILLMENT_DEADLINE_SECONDS})`, parseSeconds("--fulfillment-deadline-seconds")).option("--whitelistSettler <did>", "Settler DID to whitelist (can be specified multiple times)", collect, []).option("--provider <raw|turnkey>", "Sender signing provider", "raw").option("--solana-rpc-url <url>", "Solana RPC used to read the cluster clock for issued_at (falls back to the SOLANA_RPC_URL env var, then the public endpoint for the cluster)").addHelpText("after", `
6178
+ program.name("send-payment").description("Send a cross-chain payment through Atum Payment Gateway").version(PACKAGE_VERSION).argument("<sender>", "Depositor address (e.g., 0xF0D0...)").argument("<amount>", "Amount to send (in smallest units, e.g., 100000 for 0.10 USDC)").argument("<sourceAsset>", "Source asset in CAIP-19 format").argument("<receiver>", "Destination account address").argument("<destinationAsset>", "Destination asset in CAIP-19 format").option("--gateway <url>", "Payment gateway URL (overrides GATEWAY_URL env; default: production-testnet)").option("--request-id <id>", "Idempotency key; seeds the on-chain deposit nonce. Re-run with the SAME id to re-attempt one payment \u2014 it resolves to the original instead of paying twice. Omitted, an id is generated and printed, which makes every bare re-run a SEPARATE payment (a second charge).").option("--request-id-prefix <prefix>", "Custom request ID prefix (default: pmt-gw-cli)").option("--quote-deadline-seconds <n>", `Quote collection window in seconds (default: ${DEFAULT_PAYMENT_QUOTE_DEADLINE_SECONDS})`, parseSeconds("--quote-deadline-seconds")).option("--fulfillment-deadline-seconds <n>", `Settlement horizon in seconds (default: ${DEFAULT_FULFILLMENT_DEADLINE_SECONDS})`, parseSeconds("--fulfillment-deadline-seconds")).option("--whitelistSettler <did>", "Settler DID to whitelist (can be specified multiple times)", collect, []).option("--provider <raw|turnkey>", "Sender signing provider", "raw").option("--wait", "After submitting, keep checking the payment until it settles or fails instead of reporting it as still pending").option("--wait-seconds <n>", `How long --wait keeps checking (default: ${DEFAULT_WAIT_SECONDS}). A convenience bound only \u2014 giving up does not affect the payment`, parseSeconds("--wait-seconds")).option("--solana-rpc-url <url>", "Solana RPC used to read the cluster clock for issued_at (falls back to the SOLANA_RPC_URL env var, then the public endpoint for the cluster)").addHelpText("after", `
5987
6179
  CAIP-19 Examples:
5988
6180
  EVM ERC20: eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831
5989
6181
  Tron TRC20: tron:shasta/trc20:TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs
@@ -5998,6 +6190,23 @@ Signing (default --provider raw):
5998
6190
  Output:
5999
6191
  PaymentRequest JSON \u2192 stderr (for debugging/logging)
6000
6192
  PaymentResponse JSON \u2192 stdout (for parsing/automation)
6193
+ Outcome summary \u2192 stderr
6194
+
6195
+ With --wait the JSON is printed once the wait ends, reporting the payment's state
6196
+ as of the last check rather than as of the submit.
6197
+
6198
+ Exit codes:
6199
+ 0 completed \u2014 settled
6200
+ 2 pending \u2014 accepted, no result yet; follow it with payment-status <payment_id>,
6201
+ or use --wait
6202
+ 3 failed \u2014 terminal; a fresh attempt needs a NEW --request-id
6203
+ 1 no outcome obtained (rejected, unreachable gateway, bad arguments)
6204
+
6205
+ So \`send-payment ... && <next step>\` runs the next step only for a settled payment.
6206
+
6207
+ Exit 1 means unknown, not "nothing happened" \u2014 a request that timed out may still
6208
+ have been accepted, and you may have no payment_id to look up. Re-run with the same
6209
+ --request-id, which resolves to the payment if one was created and creates it if not.
6001
6210
 
6002
6211
  After submission, you can check payment status at eg http://PAYMENT_GW/v1/payments/PAYMENT_ID/status
6003
6212
 
@@ -6052,8 +6261,37 @@ TPUqJPASUn1zLvbLBgRZ5pBYrx7WSe5ahp on Shasta)`).action(async (sender, amount, so
6052
6261
  const response = await client.payments.submitPayment({
6053
6262
  requestBody: paymentRequest
6054
6263
  });
6055
- console.log(JSON.stringify(response, null, 2));
6056
- process.exit(0);
6264
+ let reported = response;
6265
+ if (options.wait && response.payment_id && !isTerminalStatus(response.status)) {
6266
+ const waitSeconds = options.waitSeconds ?? DEFAULT_WAIT_SECONDS;
6267
+ console.error(`waiting up to ${waitSeconds}s for ${response.payment_id} to settle...`);
6268
+ const paymentId = response.payment_id;
6269
+ const waited = await waitForTerminalStatus({
6270
+ budgetMs: waitSeconds * 1e3,
6271
+ fetchStatus: () => client.payments.getPaymentStatus({ paymentId }),
6272
+ // A blip mid-settlement is the worst moment to give up, so report and keep checking.
6273
+ onError: (err) => console.error(`status check failed, still waiting: ${err instanceof Error ? err.message : String(err)}`)
6274
+ });
6275
+ if (waited.snapshot) {
6276
+ reported = { ...reported, ...waited.snapshot };
6277
+ }
6278
+ if (waited.timedOut) {
6279
+ console.error(`still not settled after ${waitSeconds}s \u2014 reporting it as pending.`);
6280
+ }
6281
+ }
6282
+ console.log(JSON.stringify(reported, null, 2));
6283
+ const outcome = {
6284
+ status: reported.status,
6285
+ paymentId: reported.payment_id,
6286
+ requestId,
6287
+ idempotentReplay: reported.idempotent_replay,
6288
+ // Present when the gateway has recorded a reason — which includes the case where this
6289
+ // submission was deduplicated onto a payment that had already failed.
6290
+ errorCode: reported.error?.code
6291
+ };
6292
+ const { exitCode, summary } = describePaymentOutcome(outcome);
6293
+ console.error(summary);
6294
+ process.exit(exitCode);
6057
6295
  } catch (error) {
6058
6296
  if (error instanceof ApiError && error.body) {
6059
6297
  console.log(JSON.stringify(error.body, null, 2));
@@ -6067,7 +6305,7 @@ TPUqJPASUn1zLvbLBgRZ5pBYrx7WSe5ahp on Shasta)`).action(async (sender, amount, so
6067
6305
  if (requestId) {
6068
6306
  console.error(`request_id: ${requestId} (re-run with --request-id ${requestId} to retry this same payment)`);
6069
6307
  }
6070
- process.exit(1);
6308
+ process.exit(EXIT_ERROR);
6071
6309
  }
6072
6310
  });
6073
6311
  async function promptPrivateKey() {