@atumlabs/payment-gateway-client 2.1.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.
@@ -256,12 +256,14 @@ var require_utils = __commonJS({
256
256
  "../schemas/declarations/PaymentRequest/v1/bindings/typescript/sender-auth/dist/utils.js"(exports2) {
257
257
  "use strict";
258
258
  Object.defineProperty(exports2, "__esModule", { value: true });
259
- 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;
260
260
  exports2.resolvePermitDeadline = resolvePermitDeadline;
261
261
  exports2.generatePermitNonce = generatePermitNonce;
262
262
  exports2.resolvePermitNonce = resolvePermitNonce;
263
+ exports2.canonicalDepositor = canonicalDepositor;
263
264
  exports2.deterministicPermitNonce = deterministicPermitNonce2;
264
265
  exports2.deterministicSolanaNonce = deterministicSolanaNonce2;
266
+ exports2.deterministicRequestId = deterministicRequestId;
265
267
  exports2.sameAddress = sameAddress;
266
268
  exports2.assertKeyMatchesSender = assertKeyMatchesSender2;
267
269
  exports2.assertToken = assertToken;
@@ -306,6 +308,20 @@ var require_utils = __commonJS({
306
308
  const bucket = (seed >> 8n) % (1n << SOLANA_BUCKET_BITS);
307
309
  return bucket << 32n | slot;
308
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
+ }
309
325
  function sameAddress(a, b) {
310
326
  if (a.startsWith("0x") && b.startsWith("0x")) {
311
327
  return a.toLowerCase() === b.toLowerCase();
@@ -897,7 +913,7 @@ var require_evm = __commonJS({
897
913
  exports2.evmAdapter = {
898
914
  async buildAuthMessage({ sourceAsset, maxSourceAmount, sourceDefaults, deadline, nonce }) {
899
915
  if (nonce === void 0) {
900
- 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");
901
917
  }
902
918
  if (deadline === void 0) {
903
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)");
@@ -925,6 +941,10 @@ var require_evm = __commonJS({
925
941
  throw new Error("EVM adapter sign(): signed_messages[i].message_prehash is required");
926
942
  }
927
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
+ }
928
948
  const wallet = new ethers_1.Wallet(privateKey);
929
949
  return wallet.signTypedData(prehash.domain, prehash.types, prehash.message);
930
950
  },
@@ -1011,7 +1031,7 @@ var require_tron = __commonJS({
1011
1031
  exports2.tronAdapter = {
1012
1032
  async buildAuthMessage({ sourceAsset, maxSourceAmount, sourceDefaults, deadline, nonce }) {
1013
1033
  if (nonce === void 0) {
1014
- 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");
1015
1035
  }
1016
1036
  if (deadline === void 0) {
1017
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)");
@@ -1034,9 +1054,19 @@ var require_tron = __commonJS({
1034
1054
  const messageHash = (0, tvm_escrow_encoding_1.hashTvmPermitWitnessTransferFrom)(messagePrehash);
1035
1055
  return { messageScheme: "EVM_PERMIT2", messageHash, messagePrehash: JSON.stringify(messagePrehash) };
1036
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[...]`).
1037
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
+ }
1038
1068
  const wallet = new ethers_1.Wallet(privateKey.startsWith("0x") ? privateKey : "0x" + privateKey);
1039
- return wallet.signingKey.sign(signedMessage.message).serialized;
1069
+ return wallet.signTypedData(prehash.domain, prehash.types, prehash.message);
1040
1070
  },
1041
1071
  recoverSigner(signed, source) {
1042
1072
  let recovered;
@@ -2693,6 +2723,7 @@ var require_client = __commonJS({
2693
2723
  retryCounter;
2694
2724
  backoffHistogram;
2695
2725
  activityPendingCounter;
2726
+ signDurationHistogram;
2696
2727
  // The private API scalar is held only long enough to construct the stamper. After
2697
2728
  // `getStamper()` runs, this is nulled out so the hex string doesn't survive on the heap
2698
2729
  // for the client's lifetime. Web Crypto holds the imported key as an opaque CryptoKey.
@@ -2721,6 +2752,10 @@ var require_client = __commonJS({
2721
2752
  this.activityPendingCounter = meter.createCounter("turnkey.activity_pending_total", {
2722
2753
  description: "Activities that entered async polling (did not complete synchronously in the submit response), labeled by activity type"
2723
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
+ });
2724
2759
  }
2725
2760
  emitRetry(reason, path) {
2726
2761
  try {
@@ -2740,6 +2775,12 @@ var require_client = __commonJS({
2740
2775
  } catch {
2741
2776
  }
2742
2777
  }
2778
+ emitSignDuration(durationMs, activityType, outcome) {
2779
+ try {
2780
+ this.signDurationHistogram.record(durationMs, { activity_type: activityType, outcome });
2781
+ } catch {
2782
+ }
2783
+ }
2743
2784
  // ── public surface ─────────────────────────────────────────────────────────
2744
2785
  /** Boot-time reachability + auth probe. Throws TURNKEY_AUTH on bad credentials. */
2745
2786
  async whoami() {
@@ -2789,39 +2830,46 @@ var require_client = __commonJS({
2789
2830
  }
2790
2831
  // ── activity submission ────────────────────────────────────────────────────
2791
2832
  async submitActivity(activityType, endpoint, parameters) {
2792
- const body = {
2793
- type: activityType,
2794
- timestampMs: String(Date.now()),
2795
- organizationId: this.opts.organizationId,
2796
- parameters
2797
- };
2798
- const signWith = typeof parameters.signWith === "string" ? parameters.signWith : void 0;
2799
- const ctx = `activity=${activityType}${signWith ? ` signWith=${signWith}` : ""}`;
2800
- const resp = await this.queryPost(`/public/v1/submit/${endpoint}`, body);
2801
- let activity = resp.activity;
2802
- const deadline = Date.now() + POLL_TIMEOUT_MS;
2803
- let pollAttempt = 0;
2804
- if (activity?.status === "ACTIVITY_STATUS_PENDING") {
2805
- this.emitActivityPending(activityType);
2806
- }
2807
- while (activity?.status === "ACTIVITY_STATUS_PENDING") {
2808
- if (Date.now() > deadline) {
2809
- 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);
2810
2850
  }
2811
- const sleepIdx = Math.min(pollAttempt, POLL_BACKOFF_MS.length - 1);
2812
- await (0, promises_1.setTimeout)(POLL_BACKOFF_MS[sleepIdx]);
2813
- pollAttempt++;
2814
- const polled = await this.queryPost("/public/v1/query/get_activity", { organizationId: this.opts.organizationId, activityId: activity.id });
2815
- activity = polled.activity;
2816
- }
2817
- if (activity?.status !== "ACTIVITY_STATUS_COMPLETED") {
2818
- const failure = activity?.failure ?? {};
2819
- 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)}`);
2820
- }
2821
- if (activity.result == null) {
2822
- 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);
2823
2872
  }
2824
- return activity.result;
2825
2873
  }
2826
2874
  // ── HTTP layer ────────────────────────────────────────────────────────────
2827
2875
  /**
@@ -3095,17 +3143,33 @@ var require_secp256k1_helpers = __commonJS({
3095
3143
  "use strict";
3096
3144
  Object.defineProperty(exports2, "__esModule", { value: true });
3097
3145
  exports2.signSecp256k1PreimageAndVerify = signSecp256k1PreimageAndVerify;
3146
+ exports2.signSecp256k1TypedDataAndVerify = signSecp256k1TypedDataAndVerify;
3098
3147
  exports2.bytesFromHexOrBytes = bytesFromHexOrBytes;
3099
3148
  var ethers_1 = require("ethers");
3100
3149
  var km_core_1 = require_dist5();
3150
+ var EIP712_HASH_FUNCTION = "HASH_FUNCTION_NOT_APPLICABLE";
3101
3151
  async function signSecp256k1PreimageAndVerify(client, signWith, preimage, expectedEvmAddrLower) {
3102
3152
  const preimageHex = (0, ethers_1.hexlify)(preimage).slice(2);
3103
- const { r, s, v } = await client.signRawPayload({
3153
+ const parts = await client.signRawPayload({
3104
3154
  signWith,
3105
3155
  payload: preimageHex,
3106
3156
  encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
3107
3157
  hashFunction: "HASH_FUNCTION_KECCAK256"
3108
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) {
3109
3173
  let sig;
3110
3174
  try {
3111
3175
  sig = ethers_1.Signature.from({
@@ -3116,7 +3180,6 @@ var require_secp256k1_helpers = __commonJS({
3116
3180
  } catch (err) {
3117
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);
3118
3182
  }
3119
- const digestHex = (0, ethers_1.keccak256)(preimage);
3120
3183
  const recovered = (0, ethers_1.recoverAddress)(digestHex, sig).toLowerCase();
3121
3184
  if (recovered !== expectedEvmAddrLower) {
3122
3185
  throw (0, km_core_1.signerTerminal)(km_core_1.SignerErrorCode.TURNKEY_BAD_RESPONSE, `secp256k1 sign-then-recover mismatch: recovered ${recovered}, expected ${expectedEvmAddrLower}`);
@@ -3180,8 +3243,7 @@ ${msgBytes.length}`);
3180
3243
  }
3181
3244
  async signTypedData(domain, types, message) {
3182
3245
  const { signWith, lower } = this.requireAddresses();
3183
- const preimage = (0, ethers_1.getBytes)(ethers_1.TypedDataEncoder.encode(domain, types, message));
3184
- 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);
3185
3247
  }
3186
3248
  async signTransaction(tx) {
3187
3249
  const { signWith, lower } = this.requireAddresses();
@@ -3298,6 +3360,7 @@ var require_solana2 = __commonJS({
3298
3360
  };
3299
3361
  Object.defineProperty(exports2, "__esModule", { value: true });
3300
3362
  exports2.TurnkeySolanaSigner = void 0;
3363
+ exports2.encodePayload = encodePayload;
3301
3364
  var web3_js_1 = require("@solana/web3.js");
3302
3365
  var bs58_1 = __importDefault(require("bs58"));
3303
3366
  var km_core_1 = require_dist5();
@@ -3331,8 +3394,7 @@ var require_solana2 = __commonJS({
3331
3394
  }
3332
3395
  const { r, s } = await this.client.signRawPayload({
3333
3396
  signWith: this.address,
3334
- payload: Buffer.from(bytes).toString("hex"),
3335
- encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
3397
+ ...encodePayload(bytes),
3336
3398
  hashFunction: "HASH_FUNCTION_NOT_APPLICABLE"
3337
3399
  });
3338
3400
  const sig = Buffer.concat([Buffer.from(stripHex(r), "hex"), Buffer.from(stripHex(s), "hex")]);
@@ -3373,6 +3435,15 @@ var require_solana2 = __commonJS({
3373
3435
  }
3374
3436
  };
3375
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
+ }
3376
3447
  function stripHex(s) {
3377
3448
  return s.startsWith("0x") ? s.slice(2) : s;
3378
3449
  }
@@ -3440,8 +3511,7 @@ ${messageBytes.length}`);
3440
3511
  }
3441
3512
  async signTypedData(domain, types, message) {
3442
3513
  const { address, evmAddr } = this.requireReady();
3443
- const preimage = (0, ethers_1.getBytes)(ethers_1.TypedDataEncoder.encode(domain, types, message));
3444
- 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);
3445
3515
  }
3446
3516
  async signTransaction(tx) {
3447
3517
  const { address, evmAddr } = this.requireReady();
@@ -3820,7 +3890,7 @@ var require_policies = __commonJS({
3820
3890
  policyName: "allowSignRawPayload",
3821
3891
  effect: "EFFECT_ALLOW",
3822
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']`,
3823
- 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)."
3824
3894
  };
3825
3895
  }
3826
3896
  function buildDenyApproveToNonAllowedSpenderEvm(args) {
@@ -4465,7 +4535,7 @@ var require_dist6 = __commonJS({
4465
4535
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
4466
4536
  };
4467
4537
  Object.defineProperty(exports2, "__esModule", { value: true });
4468
- 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;
4469
4539
  var client_1 = require_client();
4470
4540
  Object.defineProperty(exports2, "TurnkeyClient", { enumerable: true, get: function() {
4471
4541
  return client_1.TurnkeyClient;
@@ -4508,6 +4578,9 @@ var require_dist6 = __commonJS({
4508
4578
  Object.defineProperty(exports2, "TurnkeySolanaSigner", { enumerable: true, get: function() {
4509
4579
  return solana_1.TurnkeySolanaSigner;
4510
4580
  } });
4581
+ Object.defineProperty(exports2, "encodePayload", { enumerable: true, get: function() {
4582
+ return solana_1.encodePayload;
4583
+ } });
4511
4584
  var tron_1 = require_tron2();
4512
4585
  Object.defineProperty(exports2, "TurnkeyTronSigner", { enumerable: true, get: function() {
4513
4586
  return tron_1.TurnkeyTronSigner;
@@ -4569,13 +4642,13 @@ async function signWithTurnkeyClient(client, chainPrefix, cfg, message, messageP
4569
4642
  };
4570
4643
  switch (chainPrefix) {
4571
4644
  case "eip155": {
4572
- const { domain, types, message: typedMessage } = parsePrehash(messagePrehash, "EVM");
4645
+ const { domain, types, message: typedMessage } = parsePrehash(messagePrehash, "EVM", message);
4573
4646
  const signer = new import_turnkey_signer.TurnkeyEvmSigner(client, deps);
4574
4647
  await signer.initialize();
4575
4648
  return { signature: await signer.signTypedData(domain, types, typedMessage) };
4576
4649
  }
4577
4650
  case "tron": {
4578
- const { domain, types, message: typedMessage } = parsePrehash(messagePrehash, "Tron");
4651
+ const { domain, types, message: typedMessage } = parsePrehash(messagePrehash, "Tron", message);
4579
4652
  const signer = new import_turnkey_signer.TurnkeyTronSigner(client, deps);
4580
4653
  await signer.initialize();
4581
4654
  return { signature: await signer.signTypedData(domain, types, typedMessage) };
@@ -4603,7 +4676,7 @@ function decodeSolanaMessageHex(message) {
4603
4676
  }
4604
4677
  return Uint8Array.from(Buffer.from(hex, "hex"));
4605
4678
  }
4606
- function parsePrehash(messagePrehash, label) {
4679
+ function parsePrehash(messagePrehash, label, advertisedDigest) {
4607
4680
  if (!messagePrehash) {
4608
4681
  throw new Error(`Turnkey ${label} signing requires signed_messages[0].message_prehash`);
4609
4682
  }
@@ -4617,13 +4690,21 @@ function parsePrehash(messagePrehash, label) {
4617
4690
  if (!isObject(parsed) || !isObject(parsed.domain) || !isObject(parsed.types) || !isObject(parsed.message)) {
4618
4691
  throw new Error(`Turnkey ${label} signing: message_prehash must be a JSON object with domain, types and message`);
4619
4692
  }
4620
- 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 };
4621
4701
  }
4622
- var import_turnkey_signer, TURNKEY_API_URL, DEFAULTS;
4702
+ var import_turnkey_signer, import_ethers5, TURNKEY_API_URL, DEFAULTS;
4623
4703
  var init_turnkey_impl = __esm({
4624
4704
  "src/signers/turnkey-impl.ts"() {
4625
4705
  "use strict";
4626
4706
  import_turnkey_signer = __toESM(require_dist6());
4707
+ import_ethers5 = require("ethers");
4627
4708
  TURNKEY_API_URL = "https://api.turnkey.com";
4628
4709
  DEFAULTS = {
4629
4710
  concurrencyLimit: 10,
@@ -5068,8 +5149,26 @@ var PaymentsService = class {
5068
5149
  * 2. Find optimal settlement routes across chains/assets
5069
5150
  * 3. Secure funds in escrow
5070
5151
  * 4. Coordinate with settlement agents for execution
5071
- * 5. Return a real-time result if processing completes within 30 seconds,
5072
- * 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.
5073
5172
  *
5074
5173
  * @param data The data for the request.
5075
5174
  * @param data.requestBody
@@ -5086,6 +5185,11 @@ var PaymentsService = class {
5086
5185
  400: "Invalid request",
5087
5186
  401: "Signature or sender authorization could not be verified",
5088
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
+ `,
5089
5193
  413: "Payment request too large to process",
5090
5194
  500: "Internal server error",
5091
5195
  503: "A dependency is temporarily unavailable"
@@ -5972,7 +6076,74 @@ function parseSeconds(optionName) {
5972
6076
  }
5973
6077
 
5974
6078
  // src/version.ts
5975
- var PACKAGE_VERSION = true ? "2.1.0" : "0.0.0-dev";
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
+ }
5976
6147
 
5977
6148
  // src/cli/send-payment.ts
5978
6149
  var readline = __toESM(require("readline"));
@@ -6004,7 +6175,7 @@ function loadTurnkeyConfigFromEnv() {
6004
6175
  };
6005
6176
  }
6006
6177
  var program = new import_commander.Command();
6007
- 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. 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", `
6008
6179
  CAIP-19 Examples:
6009
6180
  EVM ERC20: eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831
6010
6181
  Tron TRC20: tron:shasta/trc20:TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs
@@ -6019,6 +6190,23 @@ Signing (default --provider raw):
6019
6190
  Output:
6020
6191
  PaymentRequest JSON \u2192 stderr (for debugging/logging)
6021
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.
6022
6210
 
6023
6211
  After submission, you can check payment status at eg http://PAYMENT_GW/v1/payments/PAYMENT_ID/status
6024
6212
 
@@ -6073,8 +6261,37 @@ TPUqJPASUn1zLvbLBgRZ5pBYrx7WSe5ahp on Shasta)`).action(async (sender, amount, so
6073
6261
  const response = await client.payments.submitPayment({
6074
6262
  requestBody: paymentRequest
6075
6263
  });
6076
- console.log(JSON.stringify(response, null, 2));
6077
- 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);
6078
6295
  } catch (error) {
6079
6296
  if (error instanceof ApiError && error.body) {
6080
6297
  console.log(JSON.stringify(error.body, null, 2));
@@ -6088,7 +6305,7 @@ TPUqJPASUn1zLvbLBgRZ5pBYrx7WSe5ahp on Shasta)`).action(async (sender, amount, so
6088
6305
  if (requestId) {
6089
6306
  console.error(`request_id: ${requestId} (re-run with --request-id ${requestId} to retry this same payment)`);
6090
6307
  }
6091
- process.exit(1);
6308
+ process.exit(EXIT_ERROR);
6092
6309
  }
6093
6310
  });
6094
6311
  async function promptPrivateKey() {