Skip to content

Instantly share code, notes, and snippets.

@AlmostEfficient
Created July 22, 2025 15:23
Show Gist options
  • Select an option

  • Save AlmostEfficient/6167a7524e7a2a3e7370bfc373db3b31 to your computer and use it in GitHub Desktop.

Select an option

Save AlmostEfficient/6167a7524e7a2a3e7370bfc373db3b31 to your computer and use it in GitHub Desktop.
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