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