@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/cli/send-payment.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,
|
|
@@ -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
|
|
5072
|
-
* otherwise return a payment ID
|
|
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.
|
|
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.
|
|
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
|
-
|
|
6077
|
-
|
|
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(
|
|
6308
|
+
process.exit(EXIT_ERROR);
|
|
6092
6309
|
}
|
|
6093
6310
|
});
|
|
6094
6311
|
async function promptPrivateKey() {
|