@turnkey/core 1.11.2 → 1.13.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.
@@ -12,7 +12,20 @@ import { createWalletManager } from '../__wallet__/base.mjs';
12
12
  import { toUtf8Bytes } from 'ethers';
13
13
  import { verify } from '@turnkey/crypto';
14
14
  import { SignatureFormat } from '@turnkey/api-key-stamper';
15
+ import { encodeFunctionData } from 'viem';
15
16
 
17
+ const ERC20_TRANSFER_ABI = [
18
+ {
19
+ type: "function",
20
+ name: "transfer",
21
+ stateMutability: "nonpayable",
22
+ inputs: [
23
+ { name: "to", type: "address" },
24
+ { name: "amount", type: "uint256" },
25
+ ],
26
+ outputs: [{ name: "success", type: "bool" }],
27
+ },
28
+ ];
16
29
  class TurnkeyClient {
17
30
  constructor(config,
18
31
  // Users can pass in their own stampers, or we will create them. Should we remove this?
@@ -130,7 +143,7 @@ class TurnkeyClient {
130
143
  * @throws {TurnkeyError} If there is no active session or if there is an error during the logout process.
131
144
  */
132
145
  this.logout = async (params) => {
133
- withTurnkeyErrorHandling(async () => {
146
+ return withTurnkeyErrorHandling(async () => {
134
147
  if (params?.sessionKey) {
135
148
  const session = await this.storageManager.getSession(params.sessionKey);
136
149
  this.storageManager.clearSession(params.sessionKey);
@@ -1720,6 +1733,61 @@ class TurnkeyClient {
1720
1733
  errorCode: TurnkeyErrorCodes.SIGN_AND_SEND_TRANSACTION_ERROR,
1721
1734
  });
1722
1735
  };
1736
+ /**
1737
+ * @beta
1738
+ * * **API subject to change**
1739
+ *
1740
+ * Signs and submits an ERC20 `transfer(address,uint256)` as an Ethereum transaction
1741
+ * using a Turnkey-managed (embedded) wallet.
1742
+ *
1743
+ * This is a convenience wrapper around `ethSendTransaction`:
1744
+ * - Encodes ERC20 transfer calldata.
1745
+ * - Sends a transaction to the token contract.
1746
+ * - Returns a `sendTransactionStatusId` for polling with `pollTransactionStatus`.
1747
+ *
1748
+ * @param params.organizationId - Organization ID to execute the transaction under.
1749
+ * Defaults to the active session's organization.
1750
+ * @param params.stampWith - Optional stamper to authorize signing (e.g., passkey).
1751
+ * @param params.transfer - ERC20 transfer parameters.
1752
+ * @returns A promise resolving to the `sendTransactionStatusId`.
1753
+ * @throws {TurnkeyError} If amount encoding fails or Turnkey rejects the transaction.
1754
+ */
1755
+ this.ethSendErc20Transfer = async (params) => {
1756
+ const { organizationId, stampWith = this.config.defaultStamperType, transfer, } = params;
1757
+ const { from, to, tokenAddress, amount, caip2, nonce, gasLimit, maxFeePerGas, maxPriorityFeePerGas, sponsor, } = transfer;
1758
+ return withTurnkeyErrorHandling(async () => {
1759
+ let parsedAmount;
1760
+ try {
1761
+ parsedAmount = BigInt(amount);
1762
+ }
1763
+ catch {
1764
+ throw new TurnkeyError("Invalid ERC20 amount. Use a base-unit integer string.", TurnkeyErrorCodes.INVALID_REQUEST);
1765
+ }
1766
+ const data = encodeFunctionData({
1767
+ abi: ERC20_TRANSFER_ABI,
1768
+ functionName: "transfer",
1769
+ args: [to, parsedAmount],
1770
+ });
1771
+ return this.ethSendTransaction({
1772
+ ...(organizationId !== undefined ? { organizationId } : {}),
1773
+ ...(stampWith !== undefined ? { stampWith } : {}),
1774
+ transaction: {
1775
+ from,
1776
+ to: tokenAddress,
1777
+ caip2,
1778
+ data,
1779
+ ...(sponsor !== undefined ? { sponsor } : {}),
1780
+ ...(nonce ? { nonce } : {}),
1781
+ ...(gasLimit ? { gasLimit } : {}),
1782
+ ...(maxFeePerGas ? { maxFeePerGas } : {}),
1783
+ ...(maxPriorityFeePerGas ? { maxPriorityFeePerGas } : {}),
1784
+ },
1785
+ });
1786
+ }, {
1787
+ errorMessage: "Failed to sign and send ERC20 transfer",
1788
+ errorCode: TurnkeyErrorCodes.ETH_SEND_TRANSACTION_ERROR,
1789
+ });
1790
+ };
1723
1791
  /**
1724
1792
  * @beta
1725
1793
  * * **API subject to change**
@@ -1738,7 +1806,7 @@ class TurnkeyClient {
1738
1806
  *
1739
1807
  * - **Embedded wallets**
1740
1808
  * - Constructs the payload for Turnkey's `eth_send_transaction` endpoint.
1741
- * - Fetches nonces automatically when needed (normal nonce or Gas Station nonce).
1809
+ * - Forwards transaction fields directly to Turnkey's coordinator.
1742
1810
  * - Signs and submits the transaction through Turnkey.
1743
1811
  * - Returns a `sendTransactionStatusId`, which the caller must pass to
1744
1812
  * `pollTransactionStatus` to obtain the final result (tx hash + status).
@@ -1756,32 +1824,13 @@ class TurnkeyClient {
1756
1824
  */
1757
1825
  this.ethSendTransaction = async (params) => {
1758
1826
  const { organizationId: organizationIdFromParams, stampWith = this.config.defaultStamperType, transaction, } = params;
1759
- const { from, to, caip2, value, data, nonce, gasLimit, maxFeePerGas, maxPriorityFeePerGas, sponsor, } = transaction;
1827
+ const { from, to, caip2, value, data, nonce, gasLimit, maxFeePerGas, maxPriorityFeePerGas, sponsor, gasStationNonce, } = transaction;
1760
1828
  const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
1761
1829
  const organizationId = organizationIdFromParams || session?.organizationId;
1762
1830
  if (!organizationId) {
1763
1831
  throw new TurnkeyError("Organization ID must be provided to send a transaction", TurnkeyErrorCodes.INVALID_REQUEST);
1764
1832
  }
1765
1833
  return withTurnkeyErrorHandling(async () => {
1766
- let gasStationNonce;
1767
- let fetchedNonce;
1768
- //
1769
- // Fetch nonce(s) when needed:
1770
- // - sponsored: Gas Station nonce
1771
- // - non-sponsored: regular EIP-1559 nonce
1772
- //
1773
- if (!nonce || sponsor) {
1774
- const nonceResp = await this.httpClient.getNonces({
1775
- organizationId,
1776
- address: from,
1777
- caip2,
1778
- nonce: sponsor ? false : true,
1779
- gasStationNonce: sponsor ? true : false,
1780
- });
1781
- gasStationNonce = nonceResp.gasStationNonce;
1782
- fetchedNonce = nonceResp.nonce;
1783
- }
1784
- const finalNonce = nonce ?? fetchedNonce;
1785
1834
  //
1786
1835
  // Build Turnkey intent
1787
1836
  //
@@ -1791,15 +1840,14 @@ class TurnkeyClient {
1791
1840
  caip2,
1792
1841
  ...(value ? { value } : {}),
1793
1842
  ...(data ? { data } : {}),
1843
+ ...(nonce !== undefined ? { nonce } : {}),
1794
1844
  };
1795
1845
  if (sponsor) {
1796
1846
  intent.sponsor = true;
1797
- if (gasStationNonce)
1847
+ if (gasStationNonce !== undefined)
1798
1848
  intent.gasStationNonce = gasStationNonce;
1799
1849
  }
1800
1850
  else {
1801
- if (finalNonce !== undefined)
1802
- intent.nonce = finalNonce;
1803
1851
  if (gasLimit)
1804
1852
  intent.gasLimit = gasLimit;
1805
1853
  if (maxFeePerGas)
@@ -1824,6 +1872,69 @@ class TurnkeyClient {
1824
1872
  errorCode: TurnkeyErrorCodes.ETH_SEND_TRANSACTION_ERROR,
1825
1873
  });
1826
1874
  };
1875
+ /**
1876
+ * @beta
1877
+ * * **API subject to change**
1878
+ *
1879
+ * Signs and submits a Solana transaction using a Turnkey-managed (embedded) wallet.
1880
+ *
1881
+ * This method performs **authorization and signing**, and submits the transaction
1882
+ * to Turnkey’s coordinator. It **does not perform any polling** — callers must use
1883
+ * `pollTransactionStatus` to obtain the final on-chain result.
1884
+ *
1885
+ * Behavior:
1886
+ *
1887
+ * - **Connected wallets**
1888
+ * - Connected wallets are **not supported** by this method.
1889
+ * - They must instead use `signAndSendTransaction`.
1890
+ *
1891
+ * - **Embedded wallets**
1892
+ * - Constructs the payload for Turnkey's `sol_send_transaction` endpoint.
1893
+ * - Signs and submits the transaction through Turnkey.
1894
+ * - Returns a `sendTransactionStatusId`, which the caller must pass to
1895
+ * `pollTransactionStatus` to obtain the final result (signature + status).
1896
+ *
1897
+ * @param params.organizationId - Organization ID to execute the transaction under.
1898
+ * Defaults to the active session's organization.
1899
+ * @param params.stampWith - Optional stamper to authorize signing (e.g., passkey).
1900
+ * @param params.transaction - The Solana transaction details.
1901
+ * @returns A promise resolving to the `sendTransactionStatusId`.
1902
+ * This ID must be passed to `pollTransactionStatus`.
1903
+ * @throws {TurnkeyError} If the transaction is invalid or Turnkey rejects it.
1904
+ */
1905
+ this.solSendTransaction = async (params) => {
1906
+ const { organizationId: organizationIdFromParams, stampWith = this.config.defaultStamperType, transaction, } = params;
1907
+ const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
1908
+ const organizationId = organizationIdFromParams || session?.organizationId;
1909
+ if (!organizationId) {
1910
+ throw new TurnkeyError("Organization ID must be provided to send a transaction", TurnkeyErrorCodes.INVALID_REQUEST);
1911
+ }
1912
+ return withTurnkeyErrorHandling(async () => {
1913
+ const intent = {
1914
+ unsignedTransaction: transaction.unsignedTransaction,
1915
+ signWith: transaction.signWith,
1916
+ caip2: transaction.caip2,
1917
+ ...(transaction.sponsor !== undefined
1918
+ ? { sponsor: transaction.sponsor }
1919
+ : {}),
1920
+ ...(transaction.recentBlockhash
1921
+ ? { recentBlockhash: transaction.recentBlockhash }
1922
+ : {}),
1923
+ };
1924
+ const resp = await this.httpClient.solSendTransaction({
1925
+ ...intent,
1926
+ organizationId,
1927
+ }, stampWith);
1928
+ const id = resp.sendTransactionStatusId;
1929
+ if (!id) {
1930
+ throw new TurnkeyError("Missing sendTransactionStatusId", TurnkeyErrorCodes.SOL_SEND_TRANSACTION_ERROR);
1931
+ }
1932
+ return id;
1933
+ }, {
1934
+ errorMessage: "Failed to sign and send Solana transaction",
1935
+ errorCode: TurnkeyErrorCodes.SOL_SEND_TRANSACTION_ERROR,
1936
+ });
1937
+ };
1827
1938
  /**
1828
1939
  * Fetches the user details for the current session or a specified user.
1829
1940
  *
@@ -2835,7 +2946,7 @@ class TurnkeyClient {
2835
2946
  const { sessionToken, sessionKey = SessionKey.DefaultSessionkey } = params;
2836
2947
  if (!sessionToken)
2837
2948
  return;
2838
- withTurnkeyErrorHandling(async () => {
2949
+ return withTurnkeyErrorHandling(async () => {
2839
2950
  await this.storageManager.storeSession(sessionToken, sessionKey);
2840
2951
  }, {
2841
2952
  errorMessage: "Failed to store session",
@@ -2858,7 +2969,7 @@ class TurnkeyClient {
2858
2969
  */
2859
2970
  this.clearSession = async (params) => {
2860
2971
  const { sessionKey = SessionKey.DefaultSessionkey } = params || {};
2861
- withTurnkeyErrorHandling(async () => {
2972
+ return withTurnkeyErrorHandling(async () => {
2862
2973
  const session = await this.storageManager.getSession(sessionKey);
2863
2974
  if (session) {
2864
2975
  await Promise.all([
@@ -2886,7 +2997,7 @@ class TurnkeyClient {
2886
2997
  * @throws {TurnkeyError} If no sessions exist or if there is an error clearing all sessions.
2887
2998
  */
2888
2999
  this.clearAllSessions = async () => {
2889
- withTurnkeyErrorHandling(async () => {
3000
+ return withTurnkeyErrorHandling(async () => {
2890
3001
  const sessionKeys = await this.storageManager.listSessionKeys();
2891
3002
  if (sessionKeys.length === 0)
2892
3003
  return;
@@ -3056,7 +3167,7 @@ class TurnkeyClient {
3056
3167
  * @throws {TurnkeyError} If there is an error listing, checking, or deleting unused key pairs.
3057
3168
  */
3058
3169
  this.clearUnusedKeyPairs = async () => {
3059
- withTurnkeyErrorHandling(async () => {
3170
+ return withTurnkeyErrorHandling(async () => {
3060
3171
  const publicKeys = await this.apiKeyStamper?.listKeyPairs();
3061
3172
  if (!publicKeys || publicKeys.length === 0) {
3062
3173
  return;
@@ -3265,26 +3376,28 @@ class TurnkeyClient {
3265
3376
  * @beta
3266
3377
  * **API subject to change**
3267
3378
  *
3268
- * Polls Turnkey for the final result of a previously submitted Ethereum transaction.
3379
+ * Polls Turnkey for the final result of a previously submitted transaction.
3269
3380
  *
3270
3381
  * This function repeatedly calls `getSendTransactionStatus` until the transaction
3271
3382
  * reaches a terminal state.
3272
3383
  *
3273
3384
  * Terminal states:
3274
- * - **COMPLETED** or **INCLUDED** → resolves with `{ txHash }`
3385
+ * - **COMPLETED** or **INCLUDED** → resolves with chain-specific transaction details
3275
3386
  * - **FAILED** rejects with an error
3276
3387
  *
3277
3388
  * Behavior:
3278
3389
  *
3279
3390
  * - Queries Turnkey every 500ms.
3280
3391
  * - Stops polling automatically when a terminal state is reached.
3281
- * - Extracts the canonical on-chain hash via `resp.eth.txHash` when available.
3392
+ * - Returns the full status payload from Turnkey.
3393
+ * - When available, Ethereum transaction details are exposed at `resp.eth.txHash`.
3282
3394
  *
3283
- * @param organizationId - Organization ID under which the transaction was submitted.
3284
- * @param sendTransactionStatusId - Status ID returned by `ethSendTransaction.
3285
- * @param pollingIntervalMs - Optional polling interval in milliseconds (default: 500ms).
3395
+ * @param params.organizationId - Organization ID under which the transaction was submitted.
3396
+ * @param params.sendTransactionStatusId - Status ID returned by `ethSendTransaction` or `solSendTransaction`.
3397
+ * @param params.pollingIntervalMs - Optional polling interval in milliseconds (default: 500ms).
3398
+ * @param params.stampWith - Optional stamper to use for polling.
3286
3399
  *
3287
- * @returns A promise resolving to `{ txHash?: string }` if successful.
3400
+ * @returns A promise resolving to the transaction status payload if successful.
3288
3401
  * @throws {Error | string} If the transaction fails or is cancelled.
3289
3402
  */
3290
3403
  async pollTransactionStatus(params) {