@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.
package/dist/index.js CHANGED
@@ -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,
@@ -5146,8 +5227,26 @@ var PaymentsService = class {
5146
5227
  * 2. Find optimal settlement routes across chains/assets
5147
5228
  * 3. Secure funds in escrow
5148
5229
  * 4. Coordinate with settlement agents for execution
5149
- * 5. Return a real-time result if processing completes within 30 seconds,
5150
- * otherwise return a payment ID for status polling
5230
+ * 5. Return a real-time result if settlement completes inside the synchronous window,
5231
+ * otherwise return a payment ID and status for polling
5232
+ *
5233
+ * **Idempotency.** `request_id` is the idempotency key, scoped to the source account
5234
+ * and originator. Submitting the same `request_id` again returns the ORIGINAL payment
5235
+ * instead of charging a second time, so a client whose request timed out or failed
5236
+ * in transit can safely re-submit.
5237
+ *
5238
+ * Because a re-submission must be admissible on its own, some fields legitimately
5239
+ * differ between attempts and are excluded from the comparison: `quote_deadline` and
5240
+ * `fulfillment_deadline` (both must be in the future, so a later attempt has to move
5241
+ * them), and the `sender_auth` signature they are signed into.
5242
+ *
5243
+ * What must NOT differ is the payment's economics — source account and asset,
5244
+ * destination accounts and assets, `fulfillment_amount`, and `max_source_amount` (the
5245
+ * payer's own risk cap; lowering it on a retry describes a different payment, not the
5246
+ * same one with less headroom). Re-using one `request_id` for a genuinely different
5247
+ * payment is a client bug that would otherwise be silently collapsed onto the first
5248
+ * payment, so it is rejected with `409` and `IDEMPOTENCY_TERMS_MISMATCH`. Use a
5249
+ * distinct `request_id` per payment.
5151
5250
  *
5152
5251
  * @param data The data for the request.
5153
5252
  * @param data.requestBody
@@ -5164,6 +5263,11 @@ var PaymentsService = class {
5164
5263
  400: "Invalid request",
5165
5264
  401: "Signature or sender authorization could not be verified",
5166
5265
  403: "Payment declined during counterparty authorization",
5266
+ 409: `The \`request_id\` has already been used for a payment with different economics
5267
+ (\`IDEMPOTENCY_TERMS_MISMATCH\`). Re-submitting one \`request_id\` is how a retry is
5268
+ made safe, so it must identify one payment; use a distinct \`request_id\` per
5269
+ payment.
5270
+ `,
5167
5271
  413: "Payment request too large to process",
5168
5272
  500: "Internal server error",
5169
5273
  503: "A dependency is temporarily unavailable"
@@ -6158,11 +6262,18 @@ var $VerifiableCredential = {
6158
6262
  };
6159
6263
  var $PaymentResponse = {
6160
6264
  type: "object",
6161
- description: `Response from submitting a payment request. If processing completes within 30 seconds,
6162
- the response will include a fulfillment_confirmation with full settlement details.
6163
- Otherwise, you'll receive a payment_id that you can use to poll the
6164
- /v1/payments/{paymentId}/status endpoint for updates.
6265
+ description: `Response from submitting a payment request. If settlement completes inside the
6266
+ gateway's synchronous window, the response includes a fulfillment_confirmation with
6267
+ full settlement details. Otherwise you receive a payment_id plus the payment's
6268
+ current \`status\`, and can poll /v1/payments/{paymentId}/status for updates.
6269
+
6270
+ Read \`status\` before deciding what to do next: a \`pending\` payment is in flight and
6271
+ must NOT be re-submitted under a new request_id (that would be a second payment),
6272
+ whereas a \`failed\` or \`cancelled\` one is terminal \u2014 re-submitting it under the SAME
6273
+ request_id resolves to the same terminal payment, so a genuine re-attempt needs a
6274
+ new request_id.
6165
6275
  `,
6276
+ required: ["payment_id", "status", "idempotent_replay"],
6166
6277
  properties: {
6167
6278
  payment_id: {
6168
6279
  type: "string",
@@ -6170,8 +6281,60 @@ Otherwise, you'll receive a payment_id that you can use to poll the
6170
6281
  `,
6171
6282
  example: "550e8400-e29b-41d4-a716-446655440000"
6172
6283
  },
6284
+ status: {
6285
+ type: "string",
6286
+ enum: ["pending", "completed", "failed", "cancelled"],
6287
+ description: `Current status of the payment, with the same meaning as on
6288
+ /v1/payments/{paymentId}/status:
6289
+ - \`pending\`: accepted and being processed
6290
+ - \`completed\`: successfully delivered
6291
+ - \`failed\`: failed (funds returned if escrowed)
6292
+ - \`cancelled\`: cancelled before completion
6293
+
6294
+ Present on every response. \`completed\` is always accompanied by
6295
+ fulfillment_confirmation; \`failed\` is accompanied by \`error\` where a reason has
6296
+ been recorded.
6297
+ `
6298
+ },
6299
+ idempotent_replay: {
6300
+ type: "boolean",
6301
+ description: `Whether this response describes a payment that already existed, rather than one
6302
+ created by this request. \`true\` means the request_id had been used before and you
6303
+ are being handed the ORIGINAL payment \u2014 no second payment was created and nothing
6304
+ additional was charged.
6305
+
6306
+ **Re-attempting a payment.** Re-submit the same request and you get the same payment
6307
+ back. You may re-send the original bytes unchanged: the deadlines will have expired by
6308
+ then \u2014 \`quote_deadline\` bounds the auction and is short, while this endpoint may hold a
6309
+ submission for up to 30 seconds \u2014 and an expired deadline on a re-submission is not an
6310
+ error, because the request is resolved onto the payment that already exists rather than
6311
+ used to create one.
6312
+
6313
+ Re-signing with fresh deadlines is equally fine and returns the same payment. What you
6314
+ must NOT change is the request_id, and what you must not change under it is the
6315
+ payment's economics \u2014 source, destination, fulfillment_amount, max_source_amount \u2014 which
6316
+ are rejected with 409 IDEMPOTENCY_TERMS_MISMATCH. Deadlines and the signature are
6317
+ exempt precisely so that a re-attempt is possible.
6318
+
6319
+ A genuinely new payment needs a NEW request_id. In particular a \`failed\` payment is
6320
+ terminal: re-submitting its request_id keeps returning that dead payment, so recovering
6321
+ means starting a new one under a new key.
6322
+
6323
+ Present on every response, \`false\` for a newly created payment. A response without
6324
+ the field is from a gateway predating it, not a newly created payment.
6325
+
6326
+ This is the field to read when reconciling a suspected double charge: two
6327
+ submissions where the second reports \`true\` are one payment, whereas two both
6328
+ reporting \`false\` are two. It says nothing about how far the payment has got \u2014
6329
+ read \`status\` for that.
6330
+ `,
6331
+ example: false
6332
+ },
6173
6333
  fulfillment_confirmation: {
6174
6334
  "$ref": "#/components/schemas/FulfillmentConfirmation"
6335
+ },
6336
+ error: {
6337
+ "$ref": "#/components/schemas/PaymentError"
6175
6338
  }
6176
6339
  }
6177
6340
  };
@@ -20,11 +20,15 @@ fresh `request_id` only for a genuinely new payment.
20
20
  ### 1. Gateway dedup — enforced for every caller
21
21
 
22
22
  The gateway records your `request_id` (scoped to your source account and signing
23
- identity). If the same `request_id` arrives again, the gateway returns the
24
- original payment — same `payment_id`, same status and receipt — and never creates
25
- a second payment or broadcasts a second deposit. This holds whether you use the
26
- SDK or call the API directly, as long as you send a stable `request_id` and reuse
27
- it on retry.
23
+ identity). If the same `request_id` arrives again for the same payment, the
24
+ gateway returns the original payment — same `payment_id`, same status and receipt
25
+ — and never creates a second payment or broadcasts a second deposit. This holds
26
+ whether you use the SDK or call the API directly, as long as you send a stable
27
+ `request_id` and reuse it on retry.
28
+
29
+ "For the same payment" is the one condition: an id re-used for *different*
30
+ economics is rejected rather than replayed. See
31
+ [Reusing an id for different terms is rejected](#reusing-an-id-for-different-terms-is-rejected).
28
32
 
29
33
  ### 2. On-chain single-settlement — enforced by the blockchain
30
34
 
@@ -34,12 +38,88 @@ if** each retry carries the **same** nonce. The SDK derives that nonce from your
34
38
  `request_id`, so retries reuse it automatically. A direct caller sets the nonce
35
39
  themselves in the signed permit.
36
40
 
37
- The caveat runs in both directions. A changing `request_id` defeats both
38
- guarantees, because each attempt is then treated as a different payment — keep it
39
- stable and reuse it on retry. Equally, reusing a `request_id` with different terms
40
- (a corrected amount, a different token) returns the **original** payment rather
41
- than an error, so the new terms are silently dropped. A genuinely new payment
42
- always needs a new id.
41
+ A changing `request_id` defeats both guarantees, because each attempt is then
42
+ treated as a different payment — keep it stable and reuse it on retry.
43
+
44
+ ## Reusing an id for different terms is rejected
45
+
46
+ One `request_id` identifies one payment, so it cannot be pointed at different
47
+ economics. Re-submit an id you have already used with a changed amount, token,
48
+ account or destination and the gateway answers **`409`** with
49
+ `IDEMPOTENCY_TERMS_MISMATCH` rather than paying, and rather than quietly serving
50
+ you the original payment.
51
+
52
+ What counts as "different economics" is who pays, who receives, in which assets,
53
+ and for how much — the terms that decide where the money goes. Change any of them
54
+ and you get the `409`; the error's `message` names the category that differed. A
55
+ genuinely different payment needs a new `request_id`, which is what the error is
56
+ telling you.
57
+
58
+ Anything outside that list is taken from the payment that already exists, and the
59
+ value you sent is not applied. So a re-submission is not a way to amend a payment
60
+ in flight; it is a way to ask for the one you already made.
61
+
62
+ ### Deadlines and the signature are exempt
63
+
64
+ Two things you may freely change on a re-submission, because neither identifies
65
+ the payment:
66
+
67
+ - **Deadlines.** Re-send the original bytes unchanged and the deadlines will
68
+ usually have expired by then: `quote_deadline` bounds the auction and is short,
69
+ and `POST /v1/payments` can hold a submission for longer than that. An expired
70
+ deadline on a re-submission is **not** an error — the request resolves onto the
71
+ payment that already exists, so nothing is scheduled against it.
72
+ - **The signature.** Re-signing is fine. So is re-sending the original signature.
73
+
74
+ Which means both retry styles work: keep the exact bytes from the first attempt,
75
+ or rebuild and re-sign the same payment. What must not change is the
76
+ `request_id`.
77
+
78
+ ## Telling a retry from a first attempt
79
+
80
+ `PaymentResponse.idempotent_replay` is `true` when you have been handed a payment
81
+ that already existed, and `false` when this request created one. Read it if you
82
+ need to distinguish "the payment went through just now" from "the payment was
83
+ already there" — for logging, for reconciliation, or to avoid double-counting a
84
+ conversion.
85
+
86
+ ## Reading the outcome
87
+
88
+ `status` is on every response, and the three endings mean different things for a
89
+ retry:
90
+
91
+ | `status` | What it means | What to do |
92
+ | ---------------------- | --------------------------------- | ------------------------------------------------------ |
93
+ | `completed` | Settled. | Nothing. `fulfillment_confirmation` carries the proof. |
94
+ | `pending` | Accepted, still settling. | Re-submit the **same** `request_id`, or read the status endpoint. Do not start a new payment. |
95
+ | `failed` / `cancelled` | Terminal — it can never complete. | A fresh attempt needs a **new** `request_id`. |
96
+
97
+ `pending` is not success. Cross-chain settlement can outrun the gateway's
98
+ synchronous window, so a payment that has been accepted may still have no result
99
+ when you get your response.
100
+
101
+ A terminal failure **keeps** its `request_id`. Re-submitting that id returns the
102
+ same failed payment for good, which is why recovering from one means minting a
103
+ new id — the opposite of what a pending payment needs. Getting these two the
104
+ wrong way round is how a caller either pays twice or waits for something that
105
+ will never happen.
106
+
107
+ ### How to learn the outcome
108
+
109
+ Either works, and you can mix them:
110
+
111
+ - **Poll `GET /v1/payments/{payment_id}/status`** — cheapest once you hold a
112
+ `payment_id`. This is what the `payment-status` CLI does, and what
113
+ `send-payment --wait` does after submitting.
114
+ - **Re-submit the same request** — idempotent, and it returns the payment's
115
+ current state. Useful when you would rather not track `payment_id`, or when the
116
+ first attempt failed before you learned one.
117
+
118
+ If you are building a *merchant* that mediates someone else's HTTP request,
119
+ prefer having the payer re-attempt over polling inside that request: polling
120
+ holds the connection open for the whole settlement window. That constraint is
121
+ specific to sitting in the middle of someone else's request, and does not apply
122
+ to a backend calling this SDK directly.
43
123
 
44
124
  ## Chain-specific behavior
45
125
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atumlabs/payment-gateway-client",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "TypeScript client for Atum Payment Gateway API",
5
5
  "author": "Atum Labs, Inc.",
6
6
  "homepage": "https://github.com/Atum-Labs/protocol/tree/main/payment-gateway-client#readme",