@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/CHANGELOG.md +25 -0
- package/README.md +91 -11
- package/dist/cli/payment-status.js +100 -8
- package/dist/cli/quote-status.js +60 -7
- package/dist/cli/request-quote.js +60 -7
- package/dist/cli/send-payment.js +274 -57
- package/dist/index.d.ts +116 -17
- package/dist/index.js +219 -56
- package/docs/idempotency.md +91 -11
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
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
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
5150
|
-
* otherwise return a payment ID
|
|
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
|
|
6162
|
-
the response
|
|
6163
|
-
Otherwise
|
|
6164
|
-
/v1/payments/{paymentId}/status
|
|
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
|
};
|
package/docs/idempotency.md
CHANGED
|
@@ -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
|
|
24
|
-
original payment — same `payment_id`, same status and receipt
|
|
25
|
-
a second payment or broadcasts a second deposit. This holds
|
|
26
|
-
SDK or call the API directly, as long as you send a stable
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
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",
|