Created
July 22, 2025 15:23
-
-
Save AlmostEfficient/6167a7524e7a2a3e7370bfc373db3b31 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const preparePaymentIntent = async (amount: string, recipientAddress: string) => { | |
| if (!smartAccount) { | |
| throw new Error('No smart account found. Create one first.'); | |
| } | |
| if (!isValidSolanaAddress(recipientAddress)) { | |
| throw new Error('Invalid recipient address'); | |
| } | |
| try { | |
| // Convert USDC amount to base units (6 decimals) | |
| const baseAmount = usdcToBaseUnits(amount); | |
| const params = { | |
| amount: baseAmount, | |
| grid_user_id: smartAccount.grid_user_id, | |
| source: { | |
| smart_account_address: smartAccount.smart_account_address, | |
| currency: 'usdc', | |
| authorities: [smartAccount.policies.authorities[0].address] // Turnkey signer address | |
| }, | |
| destination: { | |
| address: recipientAddress, | |
| currency: 'usdc' | |
| } | |
| }; | |
| const paymentIntent = await gridService.preparePaymentIntent( | |
| smartAccount.smart_account_address, | |
| params, | |
| false // Use Grid v0 authorization (not MPC) to match backend | |
| ); | |
| return paymentIntent; | |
| } catch (err) { | |
| const errorMessage = err instanceof Error ? err.message : 'Failed to prepare payment intent'; | |
| setError(errorMessage); | |
| throw err; | |
| } | |
| }; | |
| // Grid v0 authorization - sign payment intent with user's passkey (uses cached session) | |
| const signGridPaymentIntent = async (intentPayload: string) => { | |
| if (!turnkeyWallet) { | |
| throw new Error('No Turnkey wallet available for signing'); | |
| } | |
| try { | |
| console.log('π Signing transaction with passkey...'); | |
| console.log('π Transaction details:', { | |
| base64Length: intentPayload.length, | |
| decodedBytesLength: atob(intentPayload).length, | |
| signerAddress: turnkeyWallet.turnkeyAddress | |
| }); | |
| // Use TurnkeyContext session-based signing (avoids repeated FaceID prompts) | |
| const signResult = await signTransactionPayload(intentPayload); | |
| // Extract and construct ED25519 signature from r,s components | |
| const signRawResult = signResult.activity.activity.result.signRawPayloadResult as any; | |
| if (!signRawResult?.r || !signRawResult?.s) { | |
| throw new Error(`Missing signature components. Available: ${Object.keys(signRawResult || {}).join(', ')}`); | |
| } | |
| // Ed25519 signature = R (32 bytes, encoded point) || S (32 bytes, little-endian scalar) | |
| const rClean = signRawResult.r.replace(/^0x/, ''); | |
| const sClean = signRawResult.s.replace(/^0x/, ''); | |
| // Convert hex to bytes to ensure proper length and format (React Native compatible) | |
| const rBytes = new Uint8Array(rClean.match(/.{1,2}/g)!.map((byte: string) => parseInt(byte, 16))); | |
| const sBytes = new Uint8Array(sClean.match(/.{1,2}/g)!.map((byte: string) => parseInt(byte, 16))); | |
| // Verify we have exactly 32 bytes each (Ed25519 requirement) | |
| if (rBytes.length !== 32) { | |
| throw new Error(`Invalid R component length: ${rBytes.length} bytes (expected 32)`); | |
| } | |
| if (sBytes.length !== 32) { | |
| throw new Error(`Invalid S component length: ${sBytes.length} bytes (expected 32)`); | |
| } | |
| // Ed25519 signature for Solana: R || S (64 bytes total) | |
| const signatureBytes = new Uint8Array(64); | |
| signatureBytes.set(rBytes, 0); | |
| signatureBytes.set(sBytes, 32); | |
| const signature = Array.from(signatureBytes).map((byte: number) => byte.toString(16).padStart(2, '0')).join('').toLowerCase(); | |
| // π¨ LOCAL SIGNATURE VERIFICATION | |
| const messageBytes = new Uint8Array(signResult.signedMessageHex.match(/.{1,2}/g)!.map((byte: string) => parseInt(byte, 16))); | |
| const publicKeyBytes = base58.decode(turnkeyWallet.turnkeyAddress); | |
| const isSignatureValid = ed25519.verify(signatureBytes, messageBytes, publicKeyBytes); | |
| console.log('π¨ LOCAL VERIFICATION:', { | |
| isSignatureValid, | |
| signerAddress: turnkeyWallet.turnkeyAddress, | |
| messageHex: signResult.signedMessageHex.substring(0, 32) + '...', | |
| }); | |
| if (!isSignatureValid) { | |
| throw new Error('LOCAL VERIFICATION FAILED: The signature generated by Turnkey is invalid.'); | |
| } | |
| console.log('π§ Ed25519 Signature Details:', { | |
| rLength: rBytes.length, | |
| sLength: sBytes.length, | |
| totalSignatureLength: signatureBytes.length, | |
| rHex: rClean.substring(0, 16) + '...', | |
| sHex: sClean.substring(0, 16) + '...' | |
| }); | |
| if (signature.length !== 128) { | |
| throw new Error(`Invalid signature length: ${signature.length} (expected 128)`); | |
| } | |
| console.log('β Transaction signed successfully'); | |
| return { | |
| signature: signature, | |
| signer: turnkeyWallet.turnkeyAddress, | |
| timestamp: new Date().toISOString(), | |
| type: 'passkey_ed25519_transaction_bytes' | |
| }; | |
| } catch (err) { | |
| console.error('β Payment intent signing failed:', err); | |
| throw new Error(`Signing failed: ${err instanceof Error ? err.message : 'Unknown error'}`); | |
| } | |
| }; | |
| // Complete payment flow: prepare β sign β confirm | |
| const sendUSDC = async (amount: string, recipientAddress: string) => { | |
| if (!smartAccount || !turnkeyWallet) { | |
| throw new Error('Smart account or Turnkey wallet not ready'); | |
| } | |
| try { | |
| console.log('π Starting USDC transfer...'); | |
| // Step 1: Prepare payment intent with Grid | |
| const paymentIntent = await preparePaymentIntent(amount, recipientAddress); | |
| console.log('β Payment intent prepared:', paymentIntent.id); | |
| // Step 2: Sign intent payload with Turnkey backend for Grid v0 | |
| const signedPayloadResult = await signGridPaymentIntent(paymentIntent.intent_payload); | |
| console.log('β Payment signed with Turnkey'); | |
| // Step 3: Confirm payment with Grid | |
| const result = await confirmPayment(paymentIntent, signedPayloadResult); | |
| console.log('β Payment confirmed with Grid'); | |
| return result; | |
| } catch (err) { | |
| console.error('β USDC transfer failed:', err); | |
| throw err; | |
| } | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment