@swype-org/react-sdk 0.1.4 → 0.1.5

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
@@ -118,6 +118,9 @@ function useSwypeConfig() {
118
118
  }
119
119
  return ctx;
120
120
  }
121
+ function useOptionalSwypeConfig() {
122
+ return useContext(SwypeContext);
123
+ }
121
124
  function useSwypeDepositAmount() {
122
125
  const ctx = useContext(SwypeContext);
123
126
  if (!ctx) {
@@ -142,6 +145,8 @@ __export(api_exports, {
142
145
  fetchChains: () => fetchChains,
143
146
  fetchProviders: () => fetchProviders,
144
147
  fetchTransfer: () => fetchTransfer,
148
+ fetchUserConfig: () => fetchUserConfig,
149
+ registerPasskey: () => registerPasskey,
145
150
  reportActionCompletion: () => reportActionCompletion,
146
151
  signTransfer: () => signTransfer,
147
152
  updateUserConfig: () => updateUserConfig,
@@ -205,19 +210,29 @@ async function createTransfer(apiBaseUrl, token, params) {
205
210
  if (!res.ok) await throwApiError(res);
206
211
  return await res.json();
207
212
  }
208
- async function fetchTransfer(apiBaseUrl, token, transferId) {
213
+ async function fetchTransfer(apiBaseUrl, token, transferId, authorizationSessionToken) {
214
+ if (!token && !authorizationSessionToken) {
215
+ throw new Error("Missing auth credentials for transfer fetch.");
216
+ }
209
217
  const res = await fetch(`${apiBaseUrl}/v1/transfers/${transferId}`, {
210
- headers: { Authorization: `Bearer ${token}` }
218
+ headers: {
219
+ ...token ? { Authorization: `Bearer ${token}` } : {},
220
+ ...authorizationSessionToken ? { "x-authorization-session-token": authorizationSessionToken } : {}
221
+ }
211
222
  });
212
223
  if (!res.ok) await throwApiError(res);
213
224
  return await res.json();
214
225
  }
215
- async function signTransfer(apiBaseUrl, token, transferId, signedUserOp) {
226
+ async function signTransfer(apiBaseUrl, token, transferId, signedUserOp, authorizationSessionToken) {
227
+ if (!token && !authorizationSessionToken) {
228
+ throw new Error("Missing auth credentials for transfer signing.");
229
+ }
216
230
  const res = await fetch(`${apiBaseUrl}/v1/transfers/${transferId}`, {
217
231
  method: "PATCH",
218
232
  headers: {
219
233
  "Content-Type": "application/json",
220
- Authorization: `Bearer ${token}`
234
+ ...token ? { Authorization: `Bearer ${token}` } : {},
235
+ ...authorizationSessionToken ? { "x-authorization-session-token": authorizationSessionToken } : {}
221
236
  },
222
237
  body: JSON.stringify({ signedUserOp })
223
238
  });
@@ -231,6 +246,24 @@ async function fetchAuthorizationSession(apiBaseUrl, sessionId) {
231
246
  if (!res.ok) await throwApiError(res);
232
247
  return await res.json();
233
248
  }
249
+ async function registerPasskey(apiBaseUrl, token, credentialId, publicKey) {
250
+ const res = await fetch(`${apiBaseUrl}/v1/users/config/passkey`, {
251
+ method: "POST",
252
+ headers: {
253
+ "Content-Type": "application/json",
254
+ Authorization: `Bearer ${token}`
255
+ },
256
+ body: JSON.stringify({ credentialId, publicKey })
257
+ });
258
+ if (!res.ok) await throwApiError(res);
259
+ }
260
+ async function fetchUserConfig(apiBaseUrl, token) {
261
+ const res = await fetch(`${apiBaseUrl}/v1/users/config`, {
262
+ headers: { Authorization: `Bearer ${token}` }
263
+ });
264
+ if (!res.ok) await throwApiError(res);
265
+ return await res.json();
266
+ }
234
267
  async function updateUserConfig(apiBaseUrl, token, config) {
235
268
  const res = await fetch(`${apiBaseUrl}/v1/users`, {
236
269
  method: "PATCH",
@@ -525,24 +558,78 @@ async function getWalletClient(config, parameters = {}) {
525
558
  const client = await getConnectorClient(config, parameters);
526
559
  return client.extend(walletActions);
527
560
  }
528
- async function waitForWalletClient(wagmiConfig2, walletClientParams = {}, maxAttempts = 15, intervalMs = 200) {
529
- for (let i = 0; i < maxAttempts; i++) {
561
+ var WALLET_CLIENT_MAX_ATTEMPTS = 15;
562
+ var WALLET_CLIENT_POLL_MS = 200;
563
+ var ACTION_POLL_INTERVAL_MS = 500;
564
+ var ACTION_POLL_MAX_RETRIES = 20;
565
+ var SIGN_PERMIT2_POLL_MS = 1e3;
566
+ var SIGN_PERMIT2_MAX_POLLS = 15;
567
+ var TRANSFER_SIGN_MAX_POLLS = 60;
568
+ function actionSuccess(action, message, data) {
569
+ return { actionId: action.id, type: action.type, status: "success", message, data };
570
+ }
571
+ function actionError(action, message) {
572
+ return { actionId: action.id, type: action.type, status: "error", message };
573
+ }
574
+ function isUserRejection(msg) {
575
+ const lower = msg.toLowerCase();
576
+ return lower.includes("rejected") || lower.includes("denied");
577
+ }
578
+ function hexToBytes(hex) {
579
+ const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
580
+ const bytes = clean.match(/.{1,2}/g).map((b) => parseInt(b, 16));
581
+ return new Uint8Array(bytes);
582
+ }
583
+ function toBase64(buffer) {
584
+ return btoa(String.fromCharCode(...new Uint8Array(buffer)));
585
+ }
586
+ function base64ToBytes(value) {
587
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
588
+ const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
589
+ const raw = atob(padded);
590
+ const bytes = new Uint8Array(raw.length);
591
+ for (let i = 0; i < raw.length; i++) {
592
+ bytes[i] = raw.charCodeAt(i);
593
+ }
594
+ return bytes;
595
+ }
596
+ function readEnvValue(name) {
597
+ const meta = import.meta;
598
+ const metaValue = meta.env?.[name];
599
+ if (typeof metaValue === "string" && metaValue.trim().length > 0) {
600
+ return metaValue.trim();
601
+ }
602
+ const processValue = globalThis.process?.env?.[name];
603
+ if (typeof processValue === "string" && processValue.trim().length > 0) {
604
+ return processValue.trim();
605
+ }
606
+ return void 0;
607
+ }
608
+ function resolvePasskeyRpId() {
609
+ const configuredDomain = readEnvValue("VITE_DOMAIN") ?? readEnvValue("SWYPE_DOMAIN");
610
+ if (configuredDomain) {
611
+ return configuredDomain.replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^\./, "").trim();
612
+ }
613
+ if (typeof window !== "undefined") {
614
+ return window.location.hostname;
615
+ }
616
+ return "localhost";
617
+ }
618
+ async function waitForWalletClient(wagmiConfig2, params = {}) {
619
+ for (let i = 0; i < WALLET_CLIENT_MAX_ATTEMPTS; i++) {
530
620
  try {
531
- return await getWalletClient(wagmiConfig2, walletClientParams);
621
+ return await getWalletClient(wagmiConfig2, params);
532
622
  } catch {
533
- if (i === maxAttempts - 1) {
623
+ if (i === WALLET_CLIENT_MAX_ATTEMPTS - 1) {
534
624
  throw new Error("Wallet not ready. Please try again.");
535
625
  }
536
- await new Promise((r) => setTimeout(r, intervalMs));
626
+ await new Promise((r) => setTimeout(r, WALLET_CLIENT_POLL_MS));
537
627
  }
538
628
  }
539
629
  throw new Error("Wallet not ready. Please try again.");
540
630
  }
541
631
  function parseSignTypedDataPayload(typedData) {
542
- const domain = typedData.domain;
543
- const types = typedData.types;
544
- const primaryType = typedData.primaryType;
545
- const message = typedData.message;
632
+ const { domain, types, primaryType, message } = typedData;
546
633
  if (!domain || typeof domain !== "object" || Array.isArray(domain)) {
547
634
  throw new Error("SIGN_PERMIT2 typedData is missing a valid domain object.");
548
635
  }
@@ -562,6 +649,46 @@ function parseSignTypedDataPayload(typedData) {
562
649
  message
563
650
  };
564
651
  }
652
+ function getPendingActions(session, completedIds) {
653
+ return session.actions.filter((a) => a.status === "PENDING" && !completedIds.has(a.id)).sort((a, b) => a.orderIndex - b.orderIndex);
654
+ }
655
+ async function createPasskeyCredential(userIdentifier) {
656
+ const challenge = new Uint8Array(32);
657
+ crypto.getRandomValues(challenge);
658
+ const rpId = resolvePasskeyRpId();
659
+ const credential = await navigator.credentials.create({
660
+ publicKey: {
661
+ challenge,
662
+ rp: { name: "Swype", id: rpId },
663
+ user: {
664
+ id: new TextEncoder().encode(userIdentifier),
665
+ name: userIdentifier,
666
+ displayName: "Swype User"
667
+ },
668
+ pubKeyCredParams: [
669
+ { alg: -7, type: "public-key" },
670
+ // ES256 (P-256)
671
+ { alg: -257, type: "public-key" }
672
+ // RS256
673
+ ],
674
+ authenticatorSelection: {
675
+ authenticatorAttachment: "platform",
676
+ residentKey: "preferred",
677
+ userVerification: "required"
678
+ },
679
+ timeout: 6e4
680
+ }
681
+ });
682
+ if (!credential) {
683
+ throw new Error("Passkey creation was cancelled.");
684
+ }
685
+ const response = credential.response;
686
+ const publicKeyBytes = response.getPublicKey?.();
687
+ return {
688
+ credentialId: toBase64(credential.rawId),
689
+ publicKey: publicKeyBytes ? toBase64(publicKeyBytes) : ""
690
+ };
691
+ }
565
692
  function useTransferPolling(intervalMs = 3e3) {
566
693
  const { apiBaseUrl } = useSwypeConfig();
567
694
  const { getAccessToken } = usePrivy();
@@ -610,8 +737,247 @@ function useTransferPolling(intervalMs = 3e3) {
610
737
  useEffect(() => () => stopPolling(), [stopPolling]);
611
738
  return { transfer, error, isPolling, startPolling, stopPolling };
612
739
  }
613
- function useAuthorizationExecutor() {
614
- const { apiBaseUrl } = useSwypeConfig();
740
+ async function executeOpenProvider(action, wagmiConfig2, connectors, connectAsync) {
741
+ try {
742
+ const account = getAccount(wagmiConfig2);
743
+ if (account.isConnected && account.address) {
744
+ const hexChainId2 = account.chainId ? `0x${account.chainId.toString(16)}` : void 0;
745
+ return actionSuccess(
746
+ action,
747
+ `Connected. Account: ${account.address}, Chain: ${hexChainId2}`,
748
+ { accounts: [account.address], chainId: hexChainId2 }
749
+ );
750
+ }
751
+ const targetId = action.metadata?.wagmiConnectorId;
752
+ const metaMaskConnector = connectors.find((c) => {
753
+ const id = c.id.toLowerCase();
754
+ const name = c.name.toLowerCase();
755
+ return id.includes("metamask") || name.includes("metamask");
756
+ });
757
+ const connector = targetId ? connectors.find((c) => c.id === targetId) ?? metaMaskConnector ?? connectors[0] : metaMaskConnector ?? connectors[0];
758
+ if (!connector) {
759
+ return actionError(action, "No wallet connector found. Please install a supported wallet.");
760
+ }
761
+ const result = await connectAsync({ connector });
762
+ const hexChainId = `0x${result.chainId.toString(16)}`;
763
+ return actionSuccess(
764
+ action,
765
+ `Connected to ${connector.name}. Account: ${result.accounts[0]}, Chain: ${hexChainId}`,
766
+ { accounts: [...result.accounts], chainId: hexChainId }
767
+ );
768
+ } catch (err) {
769
+ return actionError(
770
+ action,
771
+ err instanceof Error ? err.message : "Failed to connect wallet"
772
+ );
773
+ }
774
+ }
775
+ async function executeSelectSource(action, waitForSelection) {
776
+ try {
777
+ const options = action.metadata?.options;
778
+ const recommended = action.metadata?.recommended;
779
+ if (!options || options.length === 0) {
780
+ return actionError(
781
+ action,
782
+ "No selectable source options returned by backend."
783
+ );
784
+ }
785
+ const selection = await waitForSelection(action);
786
+ const isValidSelection = options.some(
787
+ (option) => option.chainName === selection.chainName && option.tokenSymbol === selection.tokenSymbol
788
+ );
789
+ if (!isValidSelection) {
790
+ return actionError(
791
+ action,
792
+ "Invalid source selection. Please choose one of the provided options."
793
+ );
794
+ }
795
+ return actionSuccess(
796
+ action,
797
+ `Selected ${selection.tokenSymbol} on ${selection.chainName}.`,
798
+ { selectedChainName: selection.chainName, selectedTokenSymbol: selection.tokenSymbol }
799
+ );
800
+ } catch (err) {
801
+ return actionError(
802
+ action,
803
+ err instanceof Error ? err.message : "Failed to select source"
804
+ );
805
+ }
806
+ }
807
+ async function executeSwitchChain(action, wagmiConfig2, switchChainAsync) {
808
+ try {
809
+ const account = getAccount(wagmiConfig2);
810
+ const targetChainIdHex = action.metadata?.targetChainId;
811
+ if (!targetChainIdHex) {
812
+ return actionError(action, "No targetChainId in action metadata.");
813
+ }
814
+ if (!/^0x[0-9a-fA-F]+$/.test(targetChainIdHex)) {
815
+ return actionError(
816
+ action,
817
+ `Invalid targetChainId in action metadata: ${targetChainIdHex}`
818
+ );
819
+ }
820
+ const targetChainIdNum = parseInt(targetChainIdHex, 16);
821
+ if (Number.isNaN(targetChainIdNum)) {
822
+ return actionError(
823
+ action,
824
+ `Invalid targetChainId in action metadata: ${targetChainIdHex}`
825
+ );
826
+ }
827
+ const hexChainId = `0x${targetChainIdNum.toString(16)}`;
828
+ if (account.chainId === targetChainIdNum) {
829
+ return actionSuccess(
830
+ action,
831
+ `Already on chain ${hexChainId}. Skipped.`,
832
+ { chainId: hexChainId, switched: false }
833
+ );
834
+ }
835
+ await switchChainAsync({ chainId: targetChainIdNum });
836
+ return actionSuccess(
837
+ action,
838
+ `Switched to chain ${hexChainId}.`,
839
+ { chainId: hexChainId, switched: true }
840
+ );
841
+ } catch (err) {
842
+ return actionError(
843
+ action,
844
+ err instanceof Error ? err.message : "Failed to switch chain"
845
+ );
846
+ }
847
+ }
848
+ async function executeApprovePermit2(action, wagmiConfig2) {
849
+ try {
850
+ const walletClient = await waitForWalletClient(wagmiConfig2);
851
+ const account = getAccount(wagmiConfig2);
852
+ const sender = account.address ?? walletClient.account?.address;
853
+ const expectedWallet = action.metadata?.walletAddress;
854
+ if (!sender) {
855
+ throw new Error("Wallet account not available. Please connect your wallet.");
856
+ }
857
+ if (expectedWallet && sender.toLowerCase() !== expectedWallet.toLowerCase()) {
858
+ return actionError(
859
+ action,
860
+ `Connected wallet ${sender} does not match the required source wallet ${expectedWallet}. Please switch accounts in your wallet and retry.`
861
+ );
862
+ }
863
+ const to = action.metadata?.to;
864
+ const data = action.metadata?.data;
865
+ const tokenSymbol = action.metadata?.tokenSymbol;
866
+ if (!to || !data) {
867
+ return actionError(
868
+ action,
869
+ "APPROVE_PERMIT2 metadata is missing transaction parameters (to, data)."
870
+ );
871
+ }
872
+ const txHash = await walletClient.request({
873
+ method: "eth_sendTransaction",
874
+ params: [{
875
+ from: sender,
876
+ to,
877
+ data,
878
+ value: "0x0"
879
+ }]
880
+ });
881
+ console.info(
882
+ `[swype-sdk][approve-permit2] ERC-20 approve tx sent. token=${tokenSymbol ?? to}, txHash=${txHash}`
883
+ );
884
+ return actionSuccess(
885
+ action,
886
+ `Approved Permit2 to spend ${tokenSymbol ?? "tokens"}.`,
887
+ { txHash }
888
+ );
889
+ } catch (err) {
890
+ const msg = err instanceof Error ? err.message : "Failed to approve Permit2";
891
+ return actionError(
892
+ action,
893
+ isUserRejection(msg) ? "You rejected the approval transaction. Please approve the Permit2 spending allowance in your wallet to continue." : msg
894
+ );
895
+ }
896
+ }
897
+ async function executeSignPermit2(action, wagmiConfig2, apiBaseUrl, sessionId) {
898
+ try {
899
+ const expectedWallet = action.metadata?.walletAddress;
900
+ const walletClient = await waitForWalletClient(
901
+ wagmiConfig2,
902
+ expectedWallet ? { account: expectedWallet } : {}
903
+ );
904
+ const account = getAccount(wagmiConfig2);
905
+ const connectedAddress = account.address ?? walletClient.account?.address;
906
+ const sender = expectedWallet ?? connectedAddress;
907
+ if (!sender) {
908
+ throw new Error("Wallet account not available. Please connect your wallet.");
909
+ }
910
+ if (expectedWallet && connectedAddress && connectedAddress.toLowerCase() !== expectedWallet.toLowerCase()) {
911
+ return actionError(
912
+ action,
913
+ `Connected wallet ${connectedAddress} does not match the required source wallet ${expectedWallet}. Please switch accounts in your wallet and retry.`
914
+ );
915
+ }
916
+ let typedData = action.metadata?.typedData;
917
+ const tokenSymbol = action.metadata?.tokenSymbol;
918
+ if (!typedData && sessionId) {
919
+ for (let i = 0; i < SIGN_PERMIT2_MAX_POLLS; i++) {
920
+ await new Promise((r) => setTimeout(r, SIGN_PERMIT2_POLL_MS));
921
+ const session = await fetchAuthorizationSession(apiBaseUrl, sessionId);
922
+ const updated = session.actions.find((a) => a.id === action.id);
923
+ typedData = updated?.metadata?.typedData;
924
+ if (typedData) break;
925
+ }
926
+ }
927
+ if (!typedData) {
928
+ return actionError(
929
+ action,
930
+ "SIGN_PERMIT2 metadata is missing typedData. The server may still be preparing the signing payload."
931
+ );
932
+ }
933
+ const parsed = parseSignTypedDataPayload(typedData);
934
+ console.info(
935
+ `[swype-sdk][sign-permit2] Signing typed data. expectedOwner=${expectedWallet ?? "N/A"}, senderParam=${sender}, connectedAddress=${connectedAddress ?? "N/A"}, primaryType=${parsed.primaryType}, domainChainId=${String(parsed.domain.chainId ?? "N/A")}, verifyingContract=${String(parsed.domain.verifyingContract ?? "N/A")}`
936
+ );
937
+ const signature = await walletClient.signTypedData({
938
+ account: sender,
939
+ domain: parsed.domain,
940
+ types: parsed.types,
941
+ primaryType: parsed.primaryType,
942
+ message: parsed.message
943
+ });
944
+ const recoverInput = {
945
+ domain: parsed.domain,
946
+ types: parsed.types,
947
+ primaryType: parsed.primaryType,
948
+ message: parsed.message,
949
+ signature
950
+ };
951
+ const recoveredSigner = await recoverTypedDataAddress(recoverInput);
952
+ const expectedSigner = (expectedWallet ?? sender).toLowerCase();
953
+ console.info(
954
+ `[swype-sdk][sign-permit2] Signature recovered. recoveredSigner=${recoveredSigner}, expectedSigner=${expectedSigner}`
955
+ );
956
+ if (recoveredSigner.toLowerCase() !== expectedSigner) {
957
+ return actionError(
958
+ action,
959
+ `Wallet signed with ${recoveredSigner}, but source wallet is ${expectedWallet ?? sender}. Please switch to the source wallet in MetaMask and retry.`
960
+ );
961
+ }
962
+ console.info(
963
+ `[swype-sdk][sign-permit2] Permit2 EIP-712 signature obtained. token=${tokenSymbol ?? "unknown"}`
964
+ );
965
+ return actionSuccess(
966
+ action,
967
+ `Permit2 allowance signed for ${tokenSymbol ?? "tokens"}.`,
968
+ { signature }
969
+ );
970
+ } catch (err) {
971
+ const msg = err instanceof Error ? err.message : "Failed to sign Permit2 allowance";
972
+ return actionError(
973
+ action,
974
+ isUserRejection(msg) ? "You rejected the Permit2 signature request. Please approve the signature in your wallet to allow fund transfers." : msg
975
+ );
976
+ }
977
+ }
978
+ function useAuthorizationExecutor(options) {
979
+ const swypeConfig = useOptionalSwypeConfig();
980
+ const apiBaseUrl = options?.apiBaseUrl ?? swypeConfig?.apiBaseUrl;
615
981
  const wagmiConfig2 = useConfig();
616
982
  const { connectAsync, connectors } = useConnect();
617
983
  const { switchChainAsync } = useSwitchChain();
@@ -622,6 +988,7 @@ function useAuthorizationExecutor() {
622
988
  const executingRef = useRef(false);
623
989
  const [pendingSelectSource, setPendingSelectSource] = useState(null);
624
990
  const selectSourceResolverRef = useRef(null);
991
+ const sessionIdRef = useRef(null);
625
992
  const resolveSelectSource = useCallback((selection) => {
626
993
  if (selectSourceResolverRef.current) {
627
994
  selectSourceResolverRef.current(selection);
@@ -629,487 +996,82 @@ function useAuthorizationExecutor() {
629
996
  setPendingSelectSource(null);
630
997
  }
631
998
  }, []);
632
- const sessionIdRef = useRef(null);
633
- const executeOpenProvider = useCallback(
634
- async (action) => {
635
- try {
636
- const account = getAccount(wagmiConfig2);
637
- if (account.isConnected && account.address) {
638
- const hexChainId2 = account.chainId ? `0x${account.chainId.toString(16)}` : void 0;
639
- return {
640
- actionId: action.id,
641
- type: action.type,
642
- status: "success",
643
- message: `Connected. Account: ${account.address}, Chain: ${hexChainId2}`,
644
- data: { accounts: [account.address], chainId: hexChainId2 }
645
- };
646
- }
647
- const targetId = action.metadata?.wagmiConnectorId;
648
- const metaMaskConnector = connectors.find((c) => {
649
- const id = c.id.toLowerCase();
650
- const name = c.name.toLowerCase();
651
- return id.includes("metamask") || name.includes("metamask");
652
- });
653
- const connector = targetId ? connectors.find((c) => c.id === targetId) ?? metaMaskConnector ?? connectors[0] : metaMaskConnector ?? connectors[0];
654
- if (!connector) {
655
- return {
656
- actionId: action.id,
657
- type: action.type,
658
- status: "error",
659
- message: "No wallet connector found. Please install a supported wallet."
660
- };
661
- }
662
- const result = await connectAsync({ connector });
663
- const hexChainId = `0x${result.chainId.toString(16)}`;
664
- return {
665
- actionId: action.id,
666
- type: action.type,
667
- status: "success",
668
- message: `Connected to ${connector.name}. Account: ${result.accounts[0]}, Chain: ${hexChainId}`,
669
- data: { accounts: [...result.accounts], chainId: hexChainId }
670
- };
671
- } catch (err) {
672
- return {
673
- actionId: action.id,
674
- type: action.type,
675
- status: "error",
676
- message: err instanceof Error ? err.message : "Failed to connect wallet"
677
- };
678
- }
679
- },
680
- [wagmiConfig2, connectors, connectAsync]
681
- );
682
- const executeSelectSource = useCallback(
683
- async (action) => {
684
- try {
685
- const options = action.metadata?.options;
686
- const recommended = action.metadata?.recommended;
687
- if (!options || options.length <= 1) {
688
- const selection2 = recommended ?? { chainName: "Base", tokenSymbol: "USDC" };
689
- return {
690
- actionId: action.id,
691
- type: action.type,
692
- status: "success",
693
- message: `Auto-selected ${selection2.tokenSymbol} on ${selection2.chainName}.`,
694
- data: {
695
- selectedChainName: selection2.chainName,
696
- selectedTokenSymbol: selection2.tokenSymbol
697
- }
698
- };
699
- }
700
- const selection = await new Promise((resolve) => {
701
- selectSourceResolverRef.current = resolve;
702
- setPendingSelectSource(action);
703
- });
704
- return {
705
- actionId: action.id,
706
- type: action.type,
707
- status: "success",
708
- message: `Selected ${selection.tokenSymbol} on ${selection.chainName}.`,
709
- data: {
710
- selectedChainName: selection.chainName,
711
- selectedTokenSymbol: selection.tokenSymbol
712
- }
713
- };
714
- } catch (err) {
715
- return {
716
- actionId: action.id,
717
- type: action.type,
718
- status: "error",
719
- message: err instanceof Error ? err.message : "Failed to select source"
720
- };
721
- }
722
- },
723
- []
724
- );
725
- const executeSwitchChain = useCallback(
726
- async (action) => {
727
- try {
728
- const account = getAccount(wagmiConfig2);
729
- const targetChainIdHex = action.metadata?.targetChainId;
730
- if (!targetChainIdHex) {
731
- return {
732
- actionId: action.id,
733
- type: action.type,
734
- status: "error",
735
- message: "No targetChainId in action metadata."
736
- };
737
- }
738
- const targetChainIdNum = parseInt(targetChainIdHex, 16);
739
- const hexChainId = `0x${targetChainIdNum.toString(16)}`;
740
- if (account.chainId === targetChainIdNum) {
741
- return {
742
- actionId: action.id,
743
- type: action.type,
744
- status: "success",
745
- message: `Already on chain ${hexChainId}. Skipped.`,
746
- data: { chainId: hexChainId, switched: false }
747
- };
748
- }
749
- await switchChainAsync({ chainId: targetChainIdNum });
750
- return {
751
- actionId: action.id,
752
- type: action.type,
753
- status: "success",
754
- message: `Switched to chain ${hexChainId}.`,
755
- data: { chainId: hexChainId, switched: true }
756
- };
757
- } catch (err) {
758
- return {
759
- actionId: action.id,
760
- type: action.type,
761
- status: "error",
762
- message: err instanceof Error ? err.message : "Failed to switch chain"
763
- };
764
- }
765
- },
766
- [wagmiConfig2, switchChainAsync]
767
- );
768
- const executeRegisterPasskey = useCallback(
769
- async (action) => {
770
- try {
771
- const account = getAccount(wagmiConfig2);
772
- const challenge = new Uint8Array(32);
773
- crypto.getRandomValues(challenge);
774
- const credential = await navigator.credentials.create({
775
- publicKey: {
776
- challenge,
777
- rp: {
778
- name: "Swype",
779
- id: window.location.hostname
780
- },
781
- user: {
782
- id: new TextEncoder().encode(account.address ?? "user"),
783
- name: account.address ?? "Swype User",
784
- displayName: "Swype User"
785
- },
786
- pubKeyCredParams: [
787
- { alg: -7, type: "public-key" },
788
- // ES256 (P-256)
789
- { alg: -257, type: "public-key" }
790
- // RS256
791
- ],
792
- authenticatorSelection: {
793
- authenticatorAttachment: "platform",
794
- residentKey: "preferred",
795
- userVerification: "required"
796
- },
797
- timeout: 6e4
798
- }
799
- });
800
- if (!credential) {
801
- return {
802
- actionId: action.id,
803
- type: action.type,
804
- status: "error",
805
- message: "Passkey creation was cancelled."
806
- };
807
- }
808
- const response = credential.response;
809
- const publicKeyBytes = response.getPublicKey?.();
810
- const credentialId = btoa(
811
- String.fromCharCode(...new Uint8Array(credential.rawId))
812
- );
813
- const publicKey = publicKeyBytes ? btoa(String.fromCharCode(...new Uint8Array(publicKeyBytes))) : "";
814
- return {
815
- actionId: action.id,
816
- type: action.type,
817
- status: "success",
818
- message: "Passkey created successfully.",
819
- data: {
820
- credentialId,
821
- publicKey
822
- }
823
- };
824
- } catch (err) {
825
- return {
826
- actionId: action.id,
827
- type: action.type,
828
- status: "error",
829
- message: err instanceof Error ? err.message : "Failed to create passkey"
830
- };
831
- }
832
- },
833
- [wagmiConfig2]
834
- );
835
- const executeCreateSmartAccount = useCallback(
836
- async (action) => {
837
- return {
838
- actionId: action.id,
839
- type: action.type,
840
- status: "success",
841
- message: "Smart account creation acknowledged. Server is deploying.",
842
- data: {}
843
- };
844
- },
999
+ const waitForSelection = useCallback(
1000
+ (action) => new Promise((resolve) => {
1001
+ selectSourceResolverRef.current = resolve;
1002
+ setPendingSelectSource(action);
1003
+ }),
845
1004
  []
846
1005
  );
847
- const executeApprovePermit2 = useCallback(
848
- async (action) => {
849
- try {
850
- const walletClient = await waitForWalletClient(wagmiConfig2);
851
- const account = getAccount(wagmiConfig2);
852
- const sender = account.address ?? walletClient.account?.address;
853
- const expectedWalletAddress = action.metadata?.walletAddress;
854
- if (!sender) {
855
- throw new Error("Wallet account not available. Please connect your wallet.");
856
- }
857
- if (expectedWalletAddress && sender.toLowerCase() !== expectedWalletAddress.toLowerCase()) {
858
- return {
859
- actionId: action.id,
860
- type: action.type,
861
- status: "error",
862
- message: `Connected wallet ${sender} does not match the required source wallet ${expectedWalletAddress}. Please switch accounts in your wallet and retry.`
863
- };
864
- }
865
- const to = action.metadata?.to;
866
- const data = action.metadata?.data;
867
- const tokenSymbol = action.metadata?.tokenSymbol;
868
- if (!to || !data) {
869
- return {
870
- actionId: action.id,
871
- type: action.type,
872
- status: "error",
873
- message: "APPROVE_PERMIT2 metadata is missing transaction parameters (to, data)."
874
- };
875
- }
876
- const txHash = await walletClient.request({
877
- method: "eth_sendTransaction",
878
- params: [
879
- {
880
- from: sender,
881
- to,
882
- data,
883
- value: "0x0"
884
- }
885
- ]
886
- });
887
- console.info(
888
- `[swype-sdk][approve-permit2] ERC-20 approve tx sent. token=${tokenSymbol ?? to}, txHash=${txHash}`
889
- );
890
- return {
891
- actionId: action.id,
892
- type: action.type,
893
- status: "success",
894
- message: `Approved Permit2 to spend ${tokenSymbol ?? "tokens"}.`,
895
- data: { txHash }
896
- };
897
- } catch (err) {
898
- const message = err instanceof Error ? err.message : "Failed to approve Permit2";
899
- const isRejected = message.includes("rejected") || message.includes("denied") || message.includes("user rejected");
900
- return {
901
- actionId: action.id,
902
- type: action.type,
903
- status: "error",
904
- message: isRejected ? "You rejected the approval transaction. Please approve the Permit2 spending allowance in your wallet to continue." : message
905
- };
906
- }
907
- },
908
- [wagmiConfig2]
909
- );
910
- const executeSignPermit2 = useCallback(
911
- async (action) => {
912
- try {
913
- const expectedWalletAddress = action.metadata?.walletAddress;
914
- const walletClient = await waitForWalletClient(
915
- wagmiConfig2,
916
- expectedWalletAddress ? { account: expectedWalletAddress } : {}
917
- );
918
- const account = getAccount(wagmiConfig2);
919
- const connectedAddress = account.address ?? walletClient.account?.address;
920
- const sender = expectedWalletAddress ?? connectedAddress;
921
- if (!sender) {
922
- throw new Error("Wallet account not available. Please connect your wallet.");
923
- }
924
- if (expectedWalletAddress && connectedAddress && connectedAddress.toLowerCase() !== expectedWalletAddress.toLowerCase()) {
925
- return {
926
- actionId: action.id,
927
- type: action.type,
928
- status: "error",
929
- message: `Connected wallet ${sender} does not match the required source wallet ${expectedWalletAddress}. Please switch accounts in your wallet and retry.`
930
- };
931
- }
932
- let typedData = action.metadata?.typedData;
933
- const tokenSymbol = action.metadata?.tokenSymbol;
934
- if (!typedData && sessionIdRef.current) {
935
- const POLL_INTERVAL_MS = 1e3;
936
- const MAX_POLLS = 15;
937
- for (let i = 0; i < MAX_POLLS; i++) {
938
- await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
939
- const session = await fetchAuthorizationSession(
940
- apiBaseUrl,
941
- sessionIdRef.current
942
- );
943
- const updatedAction = session.actions.find((a) => a.id === action.id);
944
- typedData = updatedAction?.metadata?.typedData;
945
- if (typedData) break;
946
- }
947
- }
948
- if (!typedData) {
949
- return {
950
- actionId: action.id,
951
- type: action.type,
952
- status: "error",
953
- message: "SIGN_PERMIT2 metadata is missing typedData. The server may still be preparing the signing payload."
954
- };
955
- }
956
- const parsedTypedData = parseSignTypedDataPayload(typedData);
957
- console.info(
958
- `[swype-sdk][sign-permit2] Signing typed data. expectedOwner=${expectedWalletAddress ?? "N/A"}, senderParam=${sender}, connectedAddress=${connectedAddress ?? "N/A"}, primaryType=${parsedTypedData.primaryType}, domainChainId=${String(parsedTypedData.domain.chainId ?? "N/A")}, verifyingContract=${String(parsedTypedData.domain.verifyingContract ?? "N/A")}`
959
- );
960
- const signature = await walletClient.signTypedData({
961
- account: sender,
962
- domain: parsedTypedData.domain,
963
- types: parsedTypedData.types,
964
- primaryType: parsedTypedData.primaryType,
965
- message: parsedTypedData.message
966
- });
967
- const recoverInput = {
968
- domain: parsedTypedData.domain,
969
- types: parsedTypedData.types,
970
- primaryType: parsedTypedData.primaryType,
971
- message: parsedTypedData.message,
972
- signature
973
- };
974
- const recoveredSigner = await recoverTypedDataAddress(recoverInput);
975
- const expectedSigner = (expectedWalletAddress ?? sender).toLowerCase();
976
- console.info(
977
- `[swype-sdk][sign-permit2] Signature recovered. recoveredSigner=${recoveredSigner}, expectedSigner=${expectedSigner}`
978
- );
979
- if (recoveredSigner.toLowerCase() !== expectedSigner) {
980
- return {
981
- actionId: action.id,
982
- type: action.type,
983
- status: "error",
984
- message: `Wallet signed with ${recoveredSigner}, but source wallet is ${expectedWalletAddress ?? sender}. Please switch to the source wallet in MetaMask and retry.`
985
- };
986
- }
987
- console.info(
988
- `[swype-sdk][sign-permit2] Permit2 EIP-712 signature obtained. token=${tokenSymbol ?? "unknown"}`
989
- );
990
- return {
991
- actionId: action.id,
992
- type: action.type,
993
- status: "success",
994
- message: `Permit2 allowance signed for ${tokenSymbol ?? "tokens"}.`,
995
- data: { signature }
996
- };
997
- } catch (err) {
998
- const message = err instanceof Error ? err.message : "Failed to sign Permit2 allowance";
999
- const isRejected = message.includes("rejected") || message.includes("denied") || message.includes("user rejected");
1000
- return {
1001
- actionId: action.id,
1002
- type: action.type,
1003
- status: "error",
1004
- message: isRejected ? "You rejected the Permit2 signature request. Please approve the signature in your wallet to allow fund transfers." : message
1005
- };
1006
- }
1007
- },
1008
- [wagmiConfig2, apiBaseUrl]
1009
- );
1010
- const executeAction = useCallback(
1006
+ const dispatchAction = useCallback(
1011
1007
  async (action) => {
1012
1008
  setCurrentAction(action);
1013
1009
  switch (action.type) {
1014
1010
  case "OPEN_PROVIDER":
1015
- return executeOpenProvider(action);
1011
+ return executeOpenProvider(action, wagmiConfig2, connectors, connectAsync);
1016
1012
  case "SELECT_SOURCE":
1017
- return executeSelectSource(action);
1013
+ return executeSelectSource(action, waitForSelection);
1018
1014
  case "SWITCH_CHAIN":
1019
- return executeSwitchChain(action);
1020
- case "REGISTER_PASSKEY":
1021
- return executeRegisterPasskey(action);
1022
- case "CREATE_SMART_ACCOUNT":
1023
- return executeCreateSmartAccount(action);
1015
+ return executeSwitchChain(action, wagmiConfig2, switchChainAsync);
1024
1016
  case "APPROVE_PERMIT2":
1025
- return executeApprovePermit2(action);
1017
+ return executeApprovePermit2(action, wagmiConfig2);
1026
1018
  case "SIGN_PERMIT2":
1027
- return executeSignPermit2(action);
1019
+ return executeSignPermit2(action, wagmiConfig2, apiBaseUrl ?? "", sessionIdRef.current);
1028
1020
  default:
1029
- return {
1030
- actionId: action.id,
1031
- type: action.type,
1032
- status: "error",
1033
- message: `Unsupported action type: ${action.type}`
1034
- };
1021
+ return actionError(action, `Unsupported action type: ${action.type}`);
1035
1022
  }
1036
1023
  },
1037
- [executeOpenProvider, executeSelectSource, executeSwitchChain, executeRegisterPasskey, executeCreateSmartAccount, executeApprovePermit2, executeSignPermit2]
1024
+ [wagmiConfig2, connectors, connectAsync, switchChainAsync, apiBaseUrl, waitForSelection]
1038
1025
  );
1039
- const executeSession = useCallback(
1040
- async (transfer) => {
1026
+ const executeSessionById = useCallback(
1027
+ async (sessionId) => {
1041
1028
  if (executingRef.current) return;
1042
1029
  executingRef.current = true;
1043
- if (!transfer.authorizationSessions || transfer.authorizationSessions.length === 0) {
1030
+ if (!sessionId) {
1044
1031
  executingRef.current = false;
1045
- throw new Error("No authorization sessions available.");
1032
+ throw new Error("No authorization session id provided.");
1033
+ }
1034
+ if (!apiBaseUrl) {
1035
+ executingRef.current = false;
1036
+ throw new Error("Missing apiBaseUrl. Provide useAuthorizationExecutor({ apiBaseUrl }) or wrap in <SwypeProvider>.");
1046
1037
  }
1047
- const sessionId = transfer.authorizationSessions[0].id;
1048
1038
  sessionIdRef.current = sessionId;
1049
1039
  setExecuting(true);
1050
1040
  setError(null);
1051
1041
  setResults([]);
1052
1042
  try {
1053
- let currentSession = await fetchAuthorizationSession(apiBaseUrl, sessionId);
1043
+ let session = await fetchAuthorizationSession(apiBaseUrl, sessionId);
1054
1044
  const allResults = [];
1055
- const completedActionIds = /* @__PURE__ */ new Set();
1056
- let pendingActions = currentSession.actions.filter((a) => a.status === "PENDING").sort((a, b) => a.orderIndex - b.orderIndex);
1057
- const ACTION_POLL_INTERVAL_MS = 500;
1058
- const ACTION_POLL_MAX_RETRIES = 20;
1059
- let actionPollRetries = 0;
1060
- while (pendingActions.length === 0 && currentSession.status !== "AUTHORIZED" && actionPollRetries < ACTION_POLL_MAX_RETRIES) {
1045
+ const completedIds = /* @__PURE__ */ new Set();
1046
+ let pending = getPendingActions(session, completedIds);
1047
+ let retries = 0;
1048
+ while (pending.length === 0 && session.status !== "AUTHORIZED" && retries < ACTION_POLL_MAX_RETRIES) {
1061
1049
  await new Promise((r) => setTimeout(r, ACTION_POLL_INTERVAL_MS));
1062
- currentSession = await fetchAuthorizationSession(apiBaseUrl, sessionId);
1063
- pendingActions = currentSession.actions.filter((a) => a.status === "PENDING").sort((a, b) => a.orderIndex - b.orderIndex);
1064
- actionPollRetries++;
1050
+ session = await fetchAuthorizationSession(apiBaseUrl, sessionId);
1051
+ pending = getPendingActions(session, completedIds);
1052
+ retries++;
1065
1053
  }
1066
- if (pendingActions.length === 0 && currentSession.status !== "AUTHORIZED") {
1054
+ if (pending.length === 0 && session.status !== "AUTHORIZED") {
1067
1055
  throw new Error("Authorization actions were not created in time. Please try again.");
1068
1056
  }
1069
- while (pendingActions.length > 0) {
1070
- const action = pendingActions[0];
1071
- if (completedActionIds.has(action.id)) break;
1072
- const result = await executeAction(action);
1057
+ while (pending.length > 0) {
1058
+ const action = pending[0];
1059
+ if (completedIds.has(action.id)) break;
1060
+ const result = await dispatchAction(action);
1073
1061
  if (result.status === "error") {
1074
1062
  allResults.push(result);
1075
1063
  setResults([...allResults]);
1076
1064
  throw new Error(result.message);
1077
1065
  }
1078
- completedActionIds.add(action.id);
1079
- const updatedSession = await reportActionCompletion(
1066
+ completedIds.add(action.id);
1067
+ allResults.push(result);
1068
+ session = await reportActionCompletion(
1080
1069
  apiBaseUrl,
1081
1070
  action.id,
1082
1071
  result.data ?? {}
1083
1072
  );
1084
- currentSession = updatedSession;
1085
- pendingActions = currentSession.actions.filter((a) => a.status === "PENDING" && !completedActionIds.has(a.id)).sort((a, b) => a.orderIndex - b.orderIndex);
1086
- if (action.type === "OPEN_PROVIDER" && pendingActions.length > 0) {
1087
- const chainResults = [result];
1088
- while (pendingActions.length > 0) {
1089
- const nextAction = pendingActions[0];
1090
- const nextResult = await executeAction(nextAction);
1091
- if (nextResult.status === "error") {
1092
- chainResults.push(nextResult);
1093
- allResults.push(...chainResults);
1094
- setResults([...allResults]);
1095
- throw new Error(nextResult.message);
1096
- }
1097
- completedActionIds.add(nextAction.id);
1098
- const nextSession = await reportActionCompletion(
1099
- apiBaseUrl,
1100
- nextAction.id,
1101
- nextResult.data ?? {}
1102
- );
1103
- currentSession = nextSession;
1104
- chainResults.push(nextResult);
1105
- pendingActions = currentSession.actions.filter((a) => a.status === "PENDING" && !completedActionIds.has(a.id)).sort((a, b) => a.orderIndex - b.orderIndex);
1106
- }
1107
- allResults.push(...chainResults);
1108
- setResults([...allResults]);
1109
- continue;
1110
- }
1111
- allResults.push(result);
1112
1073
  setResults([...allResults]);
1074
+ pending = getPendingActions(session, completedIds);
1113
1075
  }
1114
1076
  } catch (err) {
1115
1077
  const msg = err instanceof Error ? err.message : "Authorization failed";
@@ -1121,7 +1083,16 @@ function useAuthorizationExecutor() {
1121
1083
  executingRef.current = false;
1122
1084
  }
1123
1085
  },
1124
- [apiBaseUrl, executeAction]
1086
+ [apiBaseUrl, dispatchAction]
1087
+ );
1088
+ const executeSession = useCallback(
1089
+ async (transfer) => {
1090
+ if (!transfer.authorizationSessions?.length) {
1091
+ throw new Error("No authorization sessions available.");
1092
+ }
1093
+ await executeSessionById(transfer.authorizationSessions[0].id);
1094
+ },
1095
+ [executeSessionById]
1125
1096
  );
1126
1097
  return {
1127
1098
  executing,
@@ -1130,12 +1101,21 @@ function useAuthorizationExecutor() {
1130
1101
  currentAction,
1131
1102
  pendingSelectSource,
1132
1103
  resolveSelectSource,
1104
+ executeSessionById,
1133
1105
  executeSession
1134
1106
  };
1135
1107
  }
1136
- function useTransferSigning(pollIntervalMs = 2e3) {
1137
- const { apiBaseUrl } = useSwypeConfig();
1138
- const { getAccessToken } = usePrivy();
1108
+ function useTransferSigning(pollIntervalMs = 2e3, options) {
1109
+ const swypeConfig = useOptionalSwypeConfig();
1110
+ const apiBaseUrl = options?.apiBaseUrl ?? swypeConfig?.apiBaseUrl;
1111
+ const authorizationSessionToken = options?.authorizationSessionToken;
1112
+ let privyGetAccessToken;
1113
+ try {
1114
+ ({ getAccessToken: privyGetAccessToken } = usePrivy());
1115
+ } catch {
1116
+ privyGetAccessToken = void 0;
1117
+ }
1118
+ const getAccessToken = options?.getAccessToken ?? privyGetAccessToken;
1139
1119
  const [signing, setSigning] = useState(false);
1140
1120
  const [signPayload, setSignPayload] = useState(null);
1141
1121
  const [error, setError] = useState(null);
@@ -1145,14 +1125,24 @@ function useTransferSigning(pollIntervalMs = 2e3) {
1145
1125
  setError(null);
1146
1126
  setSignPayload(null);
1147
1127
  try {
1148
- const token = await getAccessToken();
1149
- if (!token) {
1128
+ if (!apiBaseUrl) {
1129
+ throw new Error("Missing apiBaseUrl. Provide useTransferSigning(_, { apiBaseUrl }) or wrap in <SwypeProvider>.");
1130
+ }
1131
+ if (!getAccessToken && !authorizationSessionToken) {
1132
+ throw new Error("Missing getAccessToken provider. Provide useTransferSigning(_, { getAccessToken }).");
1133
+ }
1134
+ const token = getAccessToken ? await getAccessToken() : null;
1135
+ if (!token && !authorizationSessionToken) {
1150
1136
  throw new Error("Could not get access token");
1151
1137
  }
1152
- const MAX_POLLS = 60;
1153
1138
  let payload = null;
1154
- for (let i = 0; i < MAX_POLLS; i++) {
1155
- const transfer = await fetchTransfer(apiBaseUrl, token, transferId);
1139
+ for (let i = 0; i < TRANSFER_SIGN_MAX_POLLS; i++) {
1140
+ const transfer = await fetchTransfer(
1141
+ apiBaseUrl,
1142
+ token ?? "",
1143
+ transferId,
1144
+ authorizationSessionToken
1145
+ );
1156
1146
  if (transfer.signPayload) {
1157
1147
  payload = transfer.signPayload;
1158
1148
  setSignPayload(payload);
@@ -1166,14 +1156,16 @@ function useTransferSigning(pollIntervalMs = 2e3) {
1166
1156
  if (!payload) {
1167
1157
  throw new Error("Timed out waiting for sign payload. Please try again.");
1168
1158
  }
1169
- const userOpHashHex = payload.userOpHash;
1170
- const hashBytes = new Uint8Array(
1171
- (userOpHashHex.startsWith("0x") ? userOpHashHex.slice(2) : userOpHashHex).match(/.{1,2}/g).map((byte) => parseInt(byte, 16))
1172
- );
1159
+ const hashBytes = hexToBytes(payload.userOpHash);
1160
+ const allowCredentials = payload.passkeyCredentialId ? [{
1161
+ type: "public-key",
1162
+ id: base64ToBytes(payload.passkeyCredentialId)
1163
+ }] : void 0;
1173
1164
  const assertion = await navigator.credentials.get({
1174
1165
  publicKey: {
1175
1166
  challenge: hashBytes,
1176
- rpId: window.location.hostname,
1167
+ rpId: resolvePasskeyRpId(),
1168
+ allowCredentials,
1177
1169
  userVerification: "required",
1178
1170
  timeout: 6e4
1179
1171
  }
@@ -1182,32 +1174,20 @@ function useTransferSigning(pollIntervalMs = 2e3) {
1182
1174
  throw new Error("Passkey authentication was cancelled.");
1183
1175
  }
1184
1176
  const response = assertion.response;
1185
- const signature = btoa(
1186
- String.fromCharCode(...new Uint8Array(response.signature))
1187
- );
1188
- const authenticatorData = btoa(
1189
- String.fromCharCode(
1190
- ...new Uint8Array(response.authenticatorData)
1191
- )
1192
- );
1193
- const clientDataJSON = btoa(
1194
- String.fromCharCode(
1195
- ...new Uint8Array(response.clientDataJSON)
1196
- )
1197
- );
1198
1177
  const signedUserOp = {
1199
1178
  ...payload.userOp,
1200
- signature,
1201
- authenticatorData,
1202
- clientDataJSON
1179
+ credentialId: toBase64(assertion.rawId),
1180
+ signature: toBase64(response.signature),
1181
+ authenticatorData: toBase64(response.authenticatorData),
1182
+ clientDataJSON: toBase64(response.clientDataJSON)
1203
1183
  };
1204
- const updatedTransfer = await signTransfer(
1184
+ return await signTransfer(
1205
1185
  apiBaseUrl,
1206
- token,
1186
+ token ?? "",
1207
1187
  transferId,
1208
- signedUserOp
1188
+ signedUserOp,
1189
+ authorizationSessionToken
1209
1190
  );
1210
- return updatedTransfer;
1211
1191
  } catch (err) {
1212
1192
  const msg = err instanceof Error ? err.message : "Failed to sign transfer";
1213
1193
  setError(msg);
@@ -1216,7 +1196,7 @@ function useTransferSigning(pollIntervalMs = 2e3) {
1216
1196
  setSigning(false);
1217
1197
  }
1218
1198
  },
1219
- [apiBaseUrl, getAccessToken, pollIntervalMs]
1199
+ [apiBaseUrl, getAccessToken, pollIntervalMs, authorizationSessionToken]
1220
1200
  );
1221
1201
  return { signing, signPayload, error, signTransfer: signTransfer2 };
1222
1202
  }
@@ -1992,6 +1972,37 @@ function computeSmartDefaults(accts, transferAmount) {
1992
1972
  }
1993
1973
  return { accountId: accts[0].id, walletId: null };
1994
1974
  }
1975
+ function parseRawBalance(rawBalance, decimals) {
1976
+ const parsed = Number(rawBalance);
1977
+ if (!Number.isFinite(parsed)) return 0;
1978
+ return parsed / 10 ** decimals;
1979
+ }
1980
+ function buildSelectSourceChoices(options) {
1981
+ const chainChoices = [];
1982
+ const chainIndexByName = /* @__PURE__ */ new Map();
1983
+ for (const option of options) {
1984
+ const chainName = option.chainName;
1985
+ const tokenSymbol = option.tokenSymbol;
1986
+ const balance = parseRawBalance(option.rawBalance, option.decimals);
1987
+ let chainChoice;
1988
+ const existingChainIdx = chainIndexByName.get(chainName);
1989
+ if (existingChainIdx === void 0) {
1990
+ chainChoice = { chainName, balance: 0, tokens: [] };
1991
+ chainIndexByName.set(chainName, chainChoices.length);
1992
+ chainChoices.push(chainChoice);
1993
+ } else {
1994
+ chainChoice = chainChoices[existingChainIdx];
1995
+ }
1996
+ chainChoice.balance += balance;
1997
+ const existingToken = chainChoice.tokens.find((token) => token.tokenSymbol === tokenSymbol);
1998
+ if (existingToken) {
1999
+ existingToken.balance += balance;
2000
+ } else {
2001
+ chainChoice.tokens.push({ tokenSymbol, balance });
2002
+ }
2003
+ }
2004
+ return chainChoices;
2005
+ }
1995
2006
  function SwypePayment({
1996
2007
  destination,
1997
2008
  onComplete,
@@ -2018,8 +2029,12 @@ function SwypePayment({
2018
2029
  });
2019
2030
  const [transfer, setTransfer] = useState(null);
2020
2031
  const [creatingTransfer, setCreatingTransfer] = useState(false);
2032
+ const [registeringPasskey, setRegisteringPasskey] = useState(false);
2021
2033
  const [mobileFlow, setMobileFlow] = useState(false);
2022
2034
  const pollingTransferIdRef = useRef(null);
2035
+ const [selectSourceChainName, setSelectSourceChainName] = useState("");
2036
+ const [selectSourceTokenSymbol, setSelectSourceTokenSymbol] = useState("");
2037
+ const initializedSelectSourceActionRef = useRef(null);
2023
2038
  const authExecutor = useAuthorizationExecutor();
2024
2039
  const polling = useTransferPolling();
2025
2040
  const transferSigning = useTransferSigning();
@@ -2031,14 +2046,36 @@ function SwypePayment({
2031
2046
  }
2032
2047
  }, [depositAmount]);
2033
2048
  useEffect(() => {
2034
- if (ready && authenticated && step === "login") {
2035
- if (depositAmount != null && depositAmount > 0) {
2036
- setStep("ready");
2037
- } else {
2038
- setStep("enter-amount");
2049
+ if (!ready || !authenticated || step !== "login") return;
2050
+ let cancelled = false;
2051
+ const checkPasskey = async () => {
2052
+ try {
2053
+ const token = await getAccessToken();
2054
+ if (!token || cancelled) return;
2055
+ const { config } = await fetchUserConfig(apiBaseUrl, token);
2056
+ if (cancelled) return;
2057
+ if (!config.passkey) {
2058
+ setStep("register-passkey");
2059
+ } else if (depositAmount != null && depositAmount > 0) {
2060
+ setStep("ready");
2061
+ } else {
2062
+ setStep("enter-amount");
2063
+ }
2064
+ } catch {
2065
+ if (!cancelled) {
2066
+ if (depositAmount != null && depositAmount > 0) {
2067
+ setStep("ready");
2068
+ } else {
2069
+ setStep("enter-amount");
2070
+ }
2071
+ }
2039
2072
  }
2040
- }
2041
- }, [ready, authenticated, step, depositAmount]);
2073
+ };
2074
+ checkPasskey();
2075
+ return () => {
2076
+ cancelled = true;
2077
+ };
2078
+ }, [ready, authenticated, step, depositAmount, apiBaseUrl, getAccessToken]);
2042
2079
  const loadingDataRef = useRef(false);
2043
2080
  useEffect(() => {
2044
2081
  if (!authenticated) return;
@@ -2112,61 +2149,43 @@ function SwypePayment({
2112
2149
  document.removeEventListener("visibilitychange", handleVisibility);
2113
2150
  };
2114
2151
  }, [mobileFlow, polling]);
2152
+ const pendingSelectSourceAction = authExecutor.pendingSelectSource;
2153
+ const selectSourceChoices = useMemo(() => {
2154
+ if (!pendingSelectSourceAction) return [];
2155
+ const options = pendingSelectSourceAction.metadata?.options ?? [];
2156
+ return buildSelectSourceChoices(options);
2157
+ }, [pendingSelectSourceAction]);
2158
+ const selectSourceRecommended = useMemo(() => {
2159
+ if (!pendingSelectSourceAction) return null;
2160
+ return pendingSelectSourceAction.metadata?.recommended ?? null;
2161
+ }, [pendingSelectSourceAction]);
2115
2162
  useEffect(() => {
2116
- if (!authExecutor.pendingSelectSource) return;
2117
- const action = authExecutor.pendingSelectSource;
2118
- const hasAdvancedOverride = advancedSettings.asset !== null || advancedSettings.chain !== null;
2119
- if (hasAdvancedOverride) {
2120
- const options = action.metadata?.options ?? [];
2121
- const recommended = action.metadata?.recommended;
2122
- const match = options.find(
2123
- (opt) => (advancedSettings.chain === null || opt.chainName === advancedSettings.chain) && (advancedSettings.asset === null || opt.tokenSymbol === advancedSettings.asset)
2124
- );
2125
- if (match) {
2126
- authExecutor.resolveSelectSource({
2127
- chainName: match.chainName,
2128
- tokenSymbol: match.tokenSymbol
2129
- });
2130
- } else if (recommended) {
2131
- authExecutor.resolveSelectSource({
2132
- chainName: recommended.chainName,
2133
- tokenSymbol: recommended.tokenSymbol
2134
- });
2135
- }
2163
+ if (!pendingSelectSourceAction) {
2164
+ initializedSelectSourceActionRef.current = null;
2165
+ setSelectSourceChainName("");
2166
+ setSelectSourceTokenSymbol("");
2167
+ return;
2168
+ }
2169
+ if (initializedSelectSourceActionRef.current === pendingSelectSourceAction.id) {
2170
+ return;
2171
+ }
2172
+ const hasRecommendedOption = !!selectSourceRecommended && selectSourceChoices.some(
2173
+ (chain) => chain.chainName === selectSourceRecommended.chainName && chain.tokens.some(
2174
+ (token) => token.tokenSymbol === selectSourceRecommended.tokenSymbol
2175
+ )
2176
+ );
2177
+ if (hasRecommendedOption && selectSourceRecommended) {
2178
+ setSelectSourceChainName(selectSourceRecommended.chainName);
2179
+ setSelectSourceTokenSymbol(selectSourceRecommended.tokenSymbol);
2180
+ } else if (selectSourceChoices.length > 0 && selectSourceChoices[0].tokens.length > 0) {
2181
+ setSelectSourceChainName(selectSourceChoices[0].chainName);
2182
+ setSelectSourceTokenSymbol(selectSourceChoices[0].tokens[0].tokenSymbol);
2136
2183
  } else {
2137
- const options = action.metadata?.options ?? [];
2138
- const recommended = action.metadata?.recommended;
2139
- const selWallet = selectedWalletId ? accounts.find((a) => a.id === selectedAccountId)?.wallets.find((w) => w.id === selectedWalletId) : null;
2140
- if (selWallet) {
2141
- const walletMatch = options.find(
2142
- (opt) => opt.chainName === selWallet.chain.name
2143
- );
2144
- if (walletMatch) {
2145
- authExecutor.resolveSelectSource({
2146
- chainName: walletMatch.chainName,
2147
- tokenSymbol: walletMatch.tokenSymbol
2148
- });
2149
- return;
2150
- }
2151
- }
2152
- if (recommended) {
2153
- authExecutor.resolveSelectSource({
2154
- chainName: recommended.chainName,
2155
- tokenSymbol: recommended.tokenSymbol
2156
- });
2157
- } else if (options.length > 0) {
2158
- authExecutor.resolveSelectSource({
2159
- chainName: options[0].chainName,
2160
- tokenSymbol: options[0].tokenSymbol
2161
- });
2162
- } else {
2163
- authExecutor.resolveSelectSource({
2164
- chainName: "Base",
2165
- tokenSymbol: "USDC"
2166
- });
2167
- }
2184
+ setSelectSourceChainName("Base");
2185
+ setSelectSourceTokenSymbol("USDC");
2168
2186
  }
2169
- }, [authExecutor, authExecutor.pendingSelectSource, advancedSettings, selectedWalletId, selectedAccountId, accounts]);
2187
+ initializedSelectSourceActionRef.current = pendingSelectSourceAction.id;
2188
+ }, [pendingSelectSourceAction, selectSourceChoices, selectSourceRecommended]);
2170
2189
  const handlePay = useCallback(async () => {
2171
2190
  const parsedAmount = parseFloat(amount);
2172
2191
  if (isNaN(parsedAmount) || parsedAmount <= 0) {
@@ -2329,6 +2348,37 @@ function SwypePayment({
2329
2348
  ]
2330
2349
  }
2331
2350
  );
2351
+ const displayedSelectSourceChoices = selectSourceChoices.length > 0 ? selectSourceChoices : [
2352
+ {
2353
+ chainName: "Base",
2354
+ balance: 0,
2355
+ tokens: [{ tokenSymbol: "USDC", balance: 0 }]
2356
+ }
2357
+ ];
2358
+ const selectedChainChoice = displayedSelectSourceChoices.find(
2359
+ (choice) => choice.chainName === selectSourceChainName
2360
+ ) ?? displayedSelectSourceChoices[0];
2361
+ const selectSourceTokenChoices = selectedChainChoice?.tokens ?? [];
2362
+ const resolvedSelectSourceChainName = selectedChainChoice?.chainName ?? selectSourceChainName;
2363
+ const resolvedSelectSourceTokenSymbol = selectSourceTokenChoices.find(
2364
+ (token) => token.tokenSymbol === selectSourceTokenSymbol
2365
+ )?.tokenSymbol ?? selectSourceTokenChoices[0]?.tokenSymbol ?? "";
2366
+ const canConfirmSelectSource = !!resolvedSelectSourceChainName && !!resolvedSelectSourceTokenSymbol;
2367
+ const handleSelectSourceChainChange = (chainName) => {
2368
+ setSelectSourceChainName(chainName);
2369
+ const nextChain = displayedSelectSourceChoices.find(
2370
+ (choice) => choice.chainName === chainName
2371
+ );
2372
+ if (!nextChain || nextChain.tokens.length === 0) {
2373
+ setSelectSourceTokenSymbol("");
2374
+ return;
2375
+ }
2376
+ const recommendedTokenForChain = selectSourceRecommended?.chainName === chainName ? selectSourceRecommended.tokenSymbol : null;
2377
+ const hasRecommendedToken = !!recommendedTokenForChain && nextChain.tokens.some((token) => token.tokenSymbol === recommendedTokenForChain);
2378
+ setSelectSourceTokenSymbol(
2379
+ hasRecommendedToken && recommendedTokenForChain ? recommendedTokenForChain : nextChain.tokens[0].tokenSymbol
2380
+ );
2381
+ };
2332
2382
  if (!ready) {
2333
2383
  return /* @__PURE__ */ jsx("div", { style: cardStyle, children: /* @__PURE__ */ jsx("div", { style: { textAlign: "center", padding: "24px 0" }, children: /* @__PURE__ */ jsx(Spinner, { label: "Initializing..." }) }) });
2334
2384
  }
@@ -2372,6 +2422,73 @@ function SwypePayment({
2372
2422
  /* @__PURE__ */ jsx("button", { style: btnPrimary, onClick: login, children: "Connect to Swype" })
2373
2423
  ] }) });
2374
2424
  }
2425
+ if (step === "register-passkey") {
2426
+ const handleRegisterPasskey = async () => {
2427
+ setRegisteringPasskey(true);
2428
+ setError(null);
2429
+ try {
2430
+ const token = await getAccessToken();
2431
+ if (!token) throw new Error("Not authenticated");
2432
+ const { credentialId, publicKey } = await createPasskeyCredential("Swype User");
2433
+ await registerPasskey(apiBaseUrl, token, credentialId, publicKey);
2434
+ if (depositAmount != null && depositAmount > 0) {
2435
+ setStep("ready");
2436
+ } else {
2437
+ setStep("enter-amount");
2438
+ }
2439
+ } catch (err) {
2440
+ const msg = err instanceof Error ? err.message : "Failed to register passkey";
2441
+ setError(msg);
2442
+ } finally {
2443
+ setRegisteringPasskey(false);
2444
+ }
2445
+ };
2446
+ return /* @__PURE__ */ jsx("div", { style: cardStyle, children: /* @__PURE__ */ jsxs("div", { style: { textAlign: "center" }, children: [
2447
+ /* @__PURE__ */ jsxs(
2448
+ "svg",
2449
+ {
2450
+ width: "48",
2451
+ height: "48",
2452
+ viewBox: "0 0 48 48",
2453
+ fill: "none",
2454
+ style: { margin: "0 auto 16px" },
2455
+ children: [
2456
+ /* @__PURE__ */ jsx("rect", { width: "48", height: "48", rx: "12", fill: tokens.accent + "20" }),
2457
+ /* @__PURE__ */ jsx(
2458
+ "path",
2459
+ {
2460
+ d: "M24 16c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 10c-4.42 0-8 1.79-8 4v2h16v-2c0-2.21-3.58-4-8-4z",
2461
+ fill: tokens.accent
2462
+ }
2463
+ )
2464
+ ]
2465
+ }
2466
+ ),
2467
+ /* @__PURE__ */ jsx("h2", { style: { ...headingStyle, marginBottom: "8px" }, children: "Set Up Passkey" }),
2468
+ /* @__PURE__ */ jsx(
2469
+ "p",
2470
+ {
2471
+ style: {
2472
+ fontSize: "0.875rem",
2473
+ color: tokens.textSecondary,
2474
+ margin: "0 0 24px 0",
2475
+ lineHeight: 1.5
2476
+ },
2477
+ children: "Create a passkey for secure, one-touch payments. This only needs to be done once."
2478
+ }
2479
+ ),
2480
+ error && /* @__PURE__ */ jsx("div", { style: errorStyle, children: error }),
2481
+ /* @__PURE__ */ jsx(
2482
+ "button",
2483
+ {
2484
+ style: registeringPasskey ? btnDisabled : btnPrimary,
2485
+ disabled: registeringPasskey,
2486
+ onClick: handleRegisterPasskey,
2487
+ children: registeringPasskey ? "Creating passkey..." : "Create Passkey"
2488
+ }
2489
+ )
2490
+ ] }) });
2491
+ }
2375
2492
  if (step === "enter-amount") {
2376
2493
  const parsedAmount = parseFloat(amount);
2377
2494
  const canContinue = !isNaN(parsedAmount) && parsedAmount > 0;
@@ -2690,6 +2807,142 @@ function SwypePayment({
2690
2807
  ] });
2691
2808
  }
2692
2809
  if (step === "processing") {
2810
+ if (pendingSelectSourceAction) {
2811
+ const chainValue = resolvedSelectSourceChainName;
2812
+ const tokenValue = resolvedSelectSourceTokenSymbol;
2813
+ return /* @__PURE__ */ jsxs("div", { style: cardStyle, children: [
2814
+ stepBadge("Select source"),
2815
+ /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", marginBottom: "16px" }, children: [
2816
+ /* @__PURE__ */ jsx("h2", { style: { ...headingStyle, marginBottom: "8px" }, children: "Select payment source" }),
2817
+ /* @__PURE__ */ jsx(
2818
+ "p",
2819
+ {
2820
+ style: {
2821
+ fontSize: "0.85rem",
2822
+ color: tokens.textSecondary,
2823
+ margin: 0,
2824
+ lineHeight: 1.5
2825
+ },
2826
+ children: "Confirm the chain and token to use for this transfer."
2827
+ }
2828
+ )
2829
+ ] }),
2830
+ /* @__PURE__ */ jsxs(
2831
+ "div",
2832
+ {
2833
+ style: {
2834
+ fontSize: "0.825rem",
2835
+ color: tokens.textSecondary,
2836
+ marginBottom: "16px",
2837
+ padding: "14px",
2838
+ background: tokens.bgInput,
2839
+ borderRadius: tokens.radius,
2840
+ border: `1px solid ${tokens.border}`
2841
+ },
2842
+ children: [
2843
+ /* @__PURE__ */ jsx(
2844
+ "label",
2845
+ {
2846
+ htmlFor: "swype-select-source-chain",
2847
+ style: {
2848
+ display: "block",
2849
+ fontSize: "0.75rem",
2850
+ fontWeight: 600,
2851
+ marginBottom: "6px",
2852
+ color: tokens.textMuted,
2853
+ textTransform: "uppercase",
2854
+ letterSpacing: "0.04em"
2855
+ },
2856
+ children: "Chain"
2857
+ }
2858
+ ),
2859
+ /* @__PURE__ */ jsx(
2860
+ "select",
2861
+ {
2862
+ id: "swype-select-source-chain",
2863
+ value: chainValue,
2864
+ onChange: (event) => handleSelectSourceChainChange(event.target.value),
2865
+ style: {
2866
+ width: "100%",
2867
+ marginBottom: "12px",
2868
+ padding: "10px 12px",
2869
+ borderRadius: tokens.radius,
2870
+ border: `1px solid ${tokens.border}`,
2871
+ background: tokens.bgCard,
2872
+ color: tokens.text,
2873
+ fontFamily: "inherit",
2874
+ fontSize: "0.875rem",
2875
+ outline: "none"
2876
+ },
2877
+ children: displayedSelectSourceChoices.map((chainChoice) => /* @__PURE__ */ jsxs("option", { value: chainChoice.chainName, children: [
2878
+ chainChoice.chainName,
2879
+ " ($",
2880
+ chainChoice.balance.toFixed(2),
2881
+ ")"
2882
+ ] }, chainChoice.chainName))
2883
+ }
2884
+ ),
2885
+ /* @__PURE__ */ jsx(
2886
+ "label",
2887
+ {
2888
+ htmlFor: "swype-select-source-token",
2889
+ style: {
2890
+ display: "block",
2891
+ fontSize: "0.75rem",
2892
+ fontWeight: 600,
2893
+ marginBottom: "6px",
2894
+ color: tokens.textMuted,
2895
+ textTransform: "uppercase",
2896
+ letterSpacing: "0.04em"
2897
+ },
2898
+ children: "Token"
2899
+ }
2900
+ ),
2901
+ /* @__PURE__ */ jsx(
2902
+ "select",
2903
+ {
2904
+ id: "swype-select-source-token",
2905
+ value: tokenValue,
2906
+ onChange: (event) => setSelectSourceTokenSymbol(event.target.value),
2907
+ style: {
2908
+ width: "100%",
2909
+ padding: "10px 12px",
2910
+ borderRadius: tokens.radius,
2911
+ border: `1px solid ${tokens.border}`,
2912
+ background: tokens.bgCard,
2913
+ color: tokens.text,
2914
+ fontFamily: "inherit",
2915
+ fontSize: "0.875rem",
2916
+ outline: "none"
2917
+ },
2918
+ children: selectSourceTokenChoices.map((tokenChoice) => /* @__PURE__ */ jsxs("option", { value: tokenChoice.tokenSymbol, children: [
2919
+ tokenChoice.tokenSymbol,
2920
+ " ($",
2921
+ tokenChoice.balance.toFixed(2),
2922
+ ")"
2923
+ ] }, tokenChoice.tokenSymbol))
2924
+ }
2925
+ )
2926
+ ]
2927
+ }
2928
+ ),
2929
+ /* @__PURE__ */ jsx(
2930
+ "button",
2931
+ {
2932
+ style: canConfirmSelectSource ? btnPrimary : btnDisabled,
2933
+ disabled: !canConfirmSelectSource,
2934
+ onClick: () => {
2935
+ if (!canConfirmSelectSource) return;
2936
+ authExecutor.resolveSelectSource({
2937
+ chainName: resolvedSelectSourceChainName,
2938
+ tokenSymbol: resolvedSelectSourceTokenSymbol
2939
+ });
2940
+ },
2941
+ children: "Confirm source"
2942
+ }
2943
+ )
2944
+ ] });
2945
+ }
2693
2946
  if (transferSigning.signing && transferSigning.signPayload) {
2694
2947
  const payload = transferSigning.signPayload;
2695
2948
  return /* @__PURE__ */ jsx("div", { style: cardStyle, children: /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: "16px 0" }, children: [
@@ -2730,11 +2983,6 @@ function SwypePayment({
2730
2983
  const currentActionType = authExecutor.currentAction?.type;
2731
2984
  const getRegistrationMessage = () => {
2732
2985
  switch (currentActionType) {
2733
- case "REGISTER_PASSKEY":
2734
- return {
2735
- label: "Creating your passkey...",
2736
- description: "Set up a passkey for secure, one-touch payments."
2737
- };
2738
2986
  case "CREATE_SMART_ACCOUNT":
2739
2987
  return {
2740
2988
  label: "Creating your smart account...",
@@ -2931,6 +3179,6 @@ function SwypePayment({
2931
3179
  return null;
2932
3180
  }
2933
3181
 
2934
- export { SwypePayment, SwypeProvider, darkTheme, getTheme, lightTheme, api_exports as swypeApi, useAuthorizationExecutor, useSwypeConfig, useSwypeDepositAmount, useTransferPolling };
3182
+ export { SwypePayment, SwypeProvider, createPasskeyCredential, darkTheme, getTheme, lightTheme, api_exports as swypeApi, useAuthorizationExecutor, useSwypeConfig, useSwypeDepositAmount, useTransferPolling, useTransferSigning };
2935
3183
  //# sourceMappingURL=index.js.map
2936
3184
  //# sourceMappingURL=index.js.map