Skip to content

Instantly share code, notes, and snippets.

@AlmostEfficient
Last active July 19, 2025 10:28
Show Gist options
  • Save AlmostEfficient/76a071f644968e22747ddc01f74559fb to your computer and use it in GitHub Desktop.
Save AlmostEfficient/76a071f644968e22747ddc01f74559fb to your computer and use it in GitHub Desktop.
import 'react-native-get-random-values';
import { v4 as uuidv4 } from 'uuid';
const GRID_BASE_URL = 'https://grid.squads.xyz/api/v0';
const GRID_API_KEY = process.env.EXPO_PUBLIC_GRID_API_KEY;
const GRID_ENVIRONMENT = process.env.EXPO_PUBLIC_GRID_ENVIRONMENT; // Change to 'production' when ready
if (!GRID_API_KEY) {
throw new Error('Missing Grid API key. Set EXPO_PUBLIC_GRID_API_KEY in your environment.');
}
// All signing logic moved to Turnkey backend - /api/signatures/mpc endpoint
export interface GridSmartAccount {
smart_account_address: string;
grid_user_id: string;
policies: {
authorities: Array<{
address: string;
permissions: string[];
}>;
admin_address: string | null;
threshold: number;
time_lock: string | null;
};
created_at: string;
}
export interface GridKyc {
id: string;
customer_id: string;
kyc_link: string;
created_at: string;
}
export interface GridPaymentIntent {
id: string;
payment_rail: string;
amount: string;
currency: string;
source: any;
destination: any;
status: string;
created_at: string;
updated_at: string;
authorities: string[];
threshold: number;
intent_payload: string;
mpc_payload: string;
}
export class GridService {
private requestQueue: Array<() => Promise<any>> = [];
private isProcessing = false;
private lastRequestTime = 0;
private readonly REQUEST_INTERVAL = 1100; // 1.1 seconds to be safe
private async processQueue() {
if (this.isProcessing || this.requestQueue.length === 0) return;
this.isProcessing = true;
while (this.requestQueue.length > 0) {
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequestTime;
if (timeSinceLastRequest < this.REQUEST_INTERVAL) {
await new Promise(resolve =>
setTimeout(resolve, this.REQUEST_INTERVAL - timeSinceLastRequest)
);
}
const request = this.requestQueue.shift();
if (request) {
this.lastRequestTime = Date.now();
try {
await request();
} catch (error) {
console.error('Queued request failed:', error);
}
}
}
this.isProcessing = false;
}
private queueRequest<T>(requestFn: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.requestQueue.push(async () => {
try {
const result = await requestFn();
resolve(result);
} catch (error) {
reject(error);
}
});
this.processQueue();
});
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<{ data: T; metadata: any }> {
const url = `${GRID_BASE_URL}${endpoint}`;
console.log(`Grid API Request: ${options.method || 'GET'} ${url}`);
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
'x-grid-environment': GRID_ENVIRONMENT || 'development',
'Authorization': `Bearer ${GRID_API_KEY}`,
...options.headers,
},
});
console.log(`Grid API Response: ${response.status} ${response.statusText}`);
if (!response.ok) {
let errorData;
try {
errorData = await response.json();
console.error('Grid API Error Response:', errorData);
} catch (parseError) {
console.error('Failed to parse error response:', parseError);
errorData = {
message: `HTTP ${response.status}: ${response.statusText}`,
status: response.status,
statusText: response.statusText
};
}
const errorMessage = errorData.message ||
errorData.error ||
errorData.details ||
`Grid API error: ${response.status} ${response.statusText}`;
throw new Error(errorMessage);
}
const responseData = await response.json();
console.log('Grid API Success Response:', responseData);
return responseData;
}
private generateIdempotencyKey(): string {
try {
return uuidv4();
} catch (error) {
// Fallback to timestamp + random number if UUID fails
const timestamp = Date.now();
const random = Math.floor(Math.random() * 1000000);
return `payment-${timestamp}-${random}`;
}
}
async requestKyc(
smartAccountAddress: string,
gridUserId: string,
email: string,
fullName: string
): Promise<GridKyc> {
const idempotencyKey = `kyc-${gridUserId}-${Date.now()}`;
const { data } = await this.request<GridKyc>(
`/grid/smart-accounts/${smartAccountAddress}/kyc`,
{
method: 'POST',
headers: {
'X-Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
grid_user_id: gridUserId,
type: 'individual',
email,
full_name: fullName,
endorsements: []
}),
}
);
return data;
}
async getKycStatus(smartAccountAddress: string, kycId: string) {
const { data } = await this.request(
`/grid/smart-accounts/${smartAccountAddress}/kyc/${kycId}`
);
return data;
}
async createVirtualAccount(smartAccountAddress: string, gridUserId: string, currency: string = 'usd') {
const idempotencyKey = `virtual-${gridUserId}-${currency}-${Date.now()}`;
const { data } = await this.request(
`/grid/smart-accounts/${smartAccountAddress}/virtual-accounts`,
{
method: 'POST',
headers: {
'X-Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
grid_user_id: gridUserId,
currency
}),
}
);
return data;
}
async createPaymentIntent(
smartAccountAddress: string,
gridUserId: string,
amount: string,
source: any,
destination: any
): Promise<GridPaymentIntent> {
const { data } = await this.request<GridPaymentIntent>(
`/grid/smart-accounts/${smartAccountAddress}/payment-intents`,
{
method: 'POST',
body: JSON.stringify({
amount,
grid_user_id: gridUserId,
source,
destination
}),
}
);
return data;
}
// New payment flow methods for chunk 2
async preparePaymentIntent(
smartAccountAddress: string,
params: {
amount: string;
grid_user_id: string;
source: any;
destination: any;
},
useMpcProvider: boolean = true
): Promise<any> {
const endpoint = useMpcProvider
? `/grid/smart-accounts/${smartAccountAddress}/payment-intents?use-mpc-provider=true`
: `/grid/smart-accounts/${smartAccountAddress}/payment-intents`;
const { data } = await this.request(endpoint, {
method: 'POST',
headers: {
'x-idempotency-key': this.generateIdempotencyKey(),
},
body: JSON.stringify(params),
});
return data;
}
async confirmPaymentIntent(
smartAccountAddress: string,
paymentIntentId: string,
payload: {
intent_payload?: string;
signature?: string; // For Grid v0 authorization
mpc_payload?: string; // Grid API expects snake_case
intentPayload?: string; // Legacy MPC
mpcPayload?: string; // Legacy MPC
},
useMpcProvider: boolean = false
): Promise<any> {
const endpoint = useMpcProvider
? `/grid/smart-accounts/${smartAccountAddress}/payment-intents/${paymentIntentId}/confirm?use-mpc-provider=true`
: `/grid/smart-accounts/${smartAccountAddress}/payment-intents/${paymentIntentId}/confirm?use-mpc-provider=false`;
const { data } = await this.request(endpoint, {
method: 'POST',
headers: {
'x-idempotency-key': this.generateIdempotencyKey(),
},
body: JSON.stringify(payload),
});
return data;
}
async getBalance(smartAccountAddress: string) {
return this.queueRequest(async () => {
try {
const { data } = await this.request(
`/grid/smart-accounts/${smartAccountAddress}/balances`
);
console.log('πŸ” Raw balance response:', JSON.stringify(data, null, 2));
const balanceArray = (data as any).balances || data;
if (Array.isArray(balanceArray) && balanceArray.length === 0) {
console.log(`πŸ”— Check smart account: https://solscan.io/account/${smartAccountAddress}`);
// Query on-chain balances as fallback
const response = await fetch('https://api.mainnet-beta.solana.com', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify([
{
// Query SOL balance
jsonrpc: '2.0',
id: 1,
method: 'getBalance',
params: [smartAccountAddress]
},
{
// Query USDC balance
jsonrpc: '2.0',
id: 2,
method: 'getTokenAccountsByOwner',
params: [
smartAccountAddress,
{
mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' // USDC mint
},
{
encoding: 'jsonParsed'
}
]
}
])
});
const [solResponse, usdcResponse] = await response.json();
// Parse SOL balance
const solBalance = (solResponse.result?.value || 0) / 1e9;
// Parse USDC balance from token accounts
let usdcBalance = 0;
if (usdcResponse.result?.value) {
for (const account of usdcResponse.result.value) {
if (account.account.data.parsed.info.mint === 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v') {
usdcBalance += Number(account.account.data.parsed.info.tokenAmount.uiAmount || 0);
}
}
}
console.log('πŸ’° On-chain balances:', { sol: solBalance, usdc: usdcBalance });
return {
usdc: usdcBalance.toString(),
sol: solBalance.toString()
};
}
if (Array.isArray(balanceArray)) {
const balances: any = {};
balanceArray.forEach((balance: any) => {
console.log('πŸ’° Balance item:', balance);
// Fix: Use symbol instead of currency, and amount_decimal instead of amount
if (balance.symbol && balance.amount_decimal !== undefined) {
balances[balance.symbol.toLowerCase()] = balance.amount_decimal;
}
});
// Ensure we always return both usdc and sol
return {
usdc: balances.usdc || '0',
sol: balances.sol || '0'
};
}
return { usdc: '0', sol: '0' };
} catch (error: any) {
console.error('Balance API error:', error);
if (error.message?.includes('not found') || error.message?.includes('404')) {
return { usdc: '0', sol: '0' };
}
throw error;
}
});
}
async getTransferHistory(smartAccountAddress: string) {
return this.queueRequest(async () => {
try {
const { data } = await this.request(
`/grid/smart-accounts/${smartAccountAddress}/payment-intents`
);
return data || [];
} catch (error: any) {
// Handle empty transfer history gracefully
if (error.message?.includes('not found') || error.message?.includes('404')) {
return []; // New account with no transfers
}
throw error;
}
});
}
// Associate smart account with MPC provider
async associateWithMpcProvider(smartAccountAddress: string, publicKey: string) {
console.log('Attempting MPC Association:', { smartAccountAddress, publicKey: publicKey.slice(0, 10) + '...' });
try {
const payload = {
public_key: publicKey,
provider: 'turnkey'
};
console.log('MPC Association Payload:', payload);
const { data } = await this.request(
`/grid/smart-accounts/${smartAccountAddress}/mpc-provider`,
{
method: 'POST',
body: JSON.stringify(payload),
}
);
console.log('MPC Association Success:', data);
return { success: true, data, message: 'Successfully associated with MPC provider' };
} catch (error: any) {
console.error('MPC Association Error Details:', {
message: error.message,
stack: error.stack,
name: error.name
});
// If already associated, that's fine
if (error.message?.includes('already associated') ||
error.message?.includes('already exists') ||
error.message?.includes('conflict')) {
return { success: true, message: 'Already associated with MPC provider' };
}
// More specific error handling
if (error.message?.includes('smart account not found') || error.message?.includes('404')) {
throw new Error('Smart account not found. Please create one first.');
}
if (error.message?.includes('invalid public key') || error.message?.includes('400')) {
throw new Error('Invalid public key format.');
}
if (error.message?.includes('401') || error.message?.includes('unauthorized')) {
throw new Error('Authentication failed. Check API key.');
}
if (error.message?.includes('403') || error.message?.includes('forbidden')) {
throw new Error('Access denied. Check permissions.');
}
// Generic error with more context
const errorMsg = error.message || 'Unknown error during MPC association';
throw new Error(`MPC association failed: ${errorMsg}`);
}
}
}
export const gridService = new GridService();
import React, { createContext, useContext, useEffect, useState } from 'react';
import { GridSmartAccount, GridKyc, gridService } from '../grid';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { usdcToBaseUnits, usdcFromBaseUnits, formatUSDC, isValidSolanaAddress } from '../../utils/helpers';
import { useTurnkey } from './TurnkeyContext';
import { useTurnkey as useTurnkeySDK, TurnkeyClient, PasskeyStamper } from '@turnkey/sdk-react-native';
import { useAuth } from './useAuth';
import { walletsApi } from '../wallets';
import { isDev } from '../app';
import { TURNKEY_CONFIG } from '../constants';
interface GridContextType {
smartAccount: GridSmartAccount | null;
kyc: GridKyc | null;
isRequestingKyc: boolean;
error: string | null;
requestKyc: (email: string, fullName: string) => Promise<GridKyc>;
getKycStatus: () => Promise<any>;
createVirtualAccount: (currency?: string) => Promise<any>;
clearGridData: () => Promise<void>;
refreshSmartAccount: () => Promise<void>;
hasSmartAccount: boolean;
testTurnkeyAuthFlow: () => Promise<{ success: boolean; message: string }>;
testUSDCFormatting: () => { success: boolean; results: any };
testAddressValidation: (address: string) => boolean;
runChunk1Tests: () => Promise<{ success: boolean; results: any[] }>;
// Chunk 2: Payment Flow Methods
preparePaymentIntent: (amount: string, recipientAddress: string) => Promise<any>;
confirmPayment: (paymentIntent: any, mpcPayload: any) => Promise<any>;
getBalance: () => Promise<{ usdc: string; sol: string }>;
getTransferHistory: () => Promise<any[]>;
// Complete payment flow (prepare β†’ sign β†’ confirm)
sendUSDC: (amount: string, recipientAddress: string) => Promise<any>;
// Turnkey backend integration
signWithTurnkey: (mpcPayload: string) => Promise<{ requestParameters: any; stamp: { publicKey: string; signature: string } }>;
// Debug helpers
checkSmartAccountOnChain: () => Promise<{ exists: boolean; hasSOL: boolean; details: any }>;
}
const GridContext = createContext<GridContextType | undefined>(undefined);
interface GridProviderProps {
children: React.ReactNode;
}
export function GridProvider({ children }: GridProviderProps) {
const [smartAccount, setSmartAccount] = useState<GridSmartAccount | null>(null);
const [kyc, setKyc] = useState<GridKyc | null>(null);
const [isRequestingKyc, setIsRequestingKyc] = useState(false);
const [error, setError] = useState<string | null>(null);
// Turnkey integration for signing
const { wallet: turnkeyWallet, signMpcPayload } = useTurnkey();
const turnkeySDK = useTurnkeySDK();
const { user } = useAuth();
// Load stored data on mount and when user changes
useEffect(() => {
loadStoredData();
}, [user]); // Re-load when user changes
const loadStoredData = async () => {
try {
// Clear state when user logs out
if (!user) {
setSmartAccount(null);
setKyc(null);
setError(null);
return;
}
const dbWallet = await walletsApi.getPrimaryWallet();
if (dbWallet?.grid_address && dbWallet?.grid_user_id) {
// Derive smart account from database wallet
const smartAccountFromDB: GridSmartAccount = {
smart_account_address: dbWallet.grid_address,
grid_user_id: dbWallet.grid_user_id,
policies: {
authorities: [{
address: dbWallet.turnkey_address,
permissions: ['CAN_INITIATE', 'CAN_VOTE', 'CAN_EXECUTE']
}],
admin_address: null,
threshold: 1,
time_lock: null
},
created_at: dbWallet.created_at || new Date().toISOString()
};
setSmartAccount(smartAccountFromDB);
}
// Keep KYC in AsyncStorage (it's not in DB)
const storedKyc = await AsyncStorage.getItem('grid_kyc');
if (storedKyc) {
setKyc(JSON.parse(storedKyc));
}
} catch (err) {
console.error('Error loading Grid data:', err);
}
};
const requestKyc = async (email: string, fullName: string): Promise<GridKyc> => {
if (!smartAccount) {
throw new Error('No smart account found. Create one first.');
}
setIsRequestingKyc(true);
setError(null);
try {
const kycData = await gridService.requestKyc(
smartAccount.smart_account_address,
smartAccount.grid_user_id,
email,
fullName
);
setKyc(kycData);
await AsyncStorage.setItem('grid_kyc', JSON.stringify(kycData));
return kycData;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to request KYC';
setError(errorMessage);
throw err;
} finally {
setIsRequestingKyc(false);
}
};
const getKycStatus = async () => {
if (!smartAccount || !kyc) {
throw new Error('No KYC found');
}
try {
const status = await gridService.getKycStatus(
smartAccount.smart_account_address,
kyc.id
);
return status;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to get KYC status';
setError(errorMessage);
throw err;
}
};
const createVirtualAccount = async (currency: string = 'usd') => {
if (!smartAccount) {
throw new Error('No smart account found');
}
try {
const virtualAccount = await gridService.createVirtualAccount(
smartAccount.smart_account_address,
smartAccount.grid_user_id,
currency
);
return virtualAccount;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to create virtual account';
setError(errorMessage);
throw err;
}
};
// Turnkey backend integration for signing MPC payloads (legacy)
const signWithTurnkey = async (mpcPayload: string) => {
if (!turnkeyWallet) {
throw new Error('No Turnkey wallet available for signing');
}
try {
console.log('πŸ” Signing MPC payload with Turnkey...');
// Use TurnkeyContext's signMpcPayload method (which handles the backend call)
const mockMode = isDev;
const result = await signMpcPayload(mpcPayload, mockMode);
console.log('βœ… Turnkey signing successful');
return {
requestParameters: result.requestParameters,
stamp: {
publicKey: result.stamp.publicKey,
signature: result.stamp.signature
}
};
} catch (err) {
console.error('❌ Turnkey signing failed:', err);
throw new Error(`Signing failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
}
};
// Grid v0 authorization - sign payment intent with user's passkey
const signGridPaymentIntent = async (intentPayload: string) => {
if (!turnkeyWallet) {
throw new Error('No Turnkey wallet available for signing');
}
try {
console.log('πŸ” Signing payment intent with user passkey...');
// ================================
// DATA FLOW DOCUMENTATION:
// ================================
// πŸ“€ GRID GIVES US: paymentIntent.intent_payload (base64 encoded Solana transaction)
// πŸ”§ WE NEED TO: Convert base64 β†’ hex for Turnkey signing
// πŸ” TURNKEY EXPECTS: hex payload with PAYLOAD_ENCODING_HEXADECIMAL
// πŸ” TURNKEY GIVES US: ED25519 signature in response.activity.result.signRawPayloadResult.r
// πŸ“₯ GRID EXPECTS: signature field in confirmPayment (hex format)
// ================================
console.log('πŸ” Signing params:', {
walletAddress: turnkeyWallet.turnkeyAddress,
intentPayloadLength: intentPayload.length,
payloadPreview: intentPayload.substring(0, 50) + '...'
});
// Debug: Let's understand what we're working with
console.log('πŸ” PAYLOAD ANALYSIS:');
console.log('πŸ“€ What Grid gave us:', {
type: typeof intentPayload,
length: intentPayload.length,
first50: intentPayload.substring(0, 50),
last10: intentPayload.substring(intentPayload.length - 10),
isBase64Like: /^[A-Za-z0-9+/]*={0,2}$/.test(intentPayload),
isHexLike: /^[0-9a-fA-F]*$/.test(intentPayload)
});
// Try multiple conversion approaches
let hexPayload: string;
let conversionMethod: string;
if (/^[0-9a-fA-F]*$/.test(intentPayload)) {
// Already hex
hexPayload = intentPayload;
conversionMethod = 'already-hex';
} else if (/^[A-Za-z0-9+/]*={0,2}$/.test(intentPayload)) {
// Looks like base64 - use React Native compatible conversion
try {
// Convert base64 to hex using native JavaScript
const binaryString = atob(intentPayload); // base64 to binary string
hexPayload = Array.from(binaryString)
.map(char => char.charCodeAt(0).toString(16).padStart(2, '0'))
.join('');
conversionMethod = 'base64-to-hex';
// Additional validation - make sure conversion worked
console.log('πŸ§ͺ Base64 conversion test:', {
originalSample: intentPayload.substring(0, 20),
binaryLength: binaryString.length,
hexSample: hexPayload.substring(0, 40)
});
} catch (err) {
console.error('Base64 conversion failed:', err);
// Fallback: treat as UTF-8 string and convert to hex
hexPayload = Array.from(intentPayload)
.map(char => char.charCodeAt(0).toString(16).padStart(2, '0'))
.join('');
conversionMethod = 'utf8-to-hex';
}
} else {
// Unknown format, try as UTF-8
hexPayload = Array.from(intentPayload)
.map(char => char.charCodeAt(0).toString(16).padStart(2, '0'))
.join('');
conversionMethod = 'utf8-to-hex-fallback';
}
console.log('πŸ”„ CONVERSION RESULT:', {
method: conversionMethod,
originalLength: intentPayload.length,
hexLength: hexPayload.length,
hexFirst50: hexPayload.substring(0, 50),
isValidHex: /^[0-9a-fA-F]*$/.test(hexPayload),
// Check if conversion actually changed anything
conversionWorked: hexPayload !== intentPayload
});
// SAFETY CHECK: If conversion didn't work, fail fast
if (hexPayload === intentPayload && conversionMethod === 'base64-to-hex') {
throw new Error('Base64 to hex conversion failed - payload unchanged');
}
// Create passkey stamper (this triggers the passkey prompt)
const stamper = new PasskeyStamper({
rpId: TURNKEY_CONFIG.RP_ID,
});
// Create Turnkey client with passkey stamper
const httpClient = new TurnkeyClient(
{ baseUrl: TURNKEY_CONFIG.TURNKEY_BASE_URL },
stamper
);
console.log('πŸ”„ Signing with fresh TurnkeyClient...');
console.log('πŸ” Turnkey signing params:', {
organizationId: turnkeyWallet.subOrganizationId,
signWith: turnkeyWallet.turnkeyAddress,
payloadLength: hexPayload.length,
encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
hashFunction: "HASH_FUNCTION_NOT_APPLICABLE" // ED25519 handles hashing internally
});
// Sign the raw payload using the passkey
const signResult = await httpClient.signRawPayload({
type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
timestampMs: Date.now().toString(),
organizationId: turnkeyWallet.subOrganizationId, // Use the wallet's sub-org ID
parameters: {
signWith: turnkeyWallet.turnkeyAddress,
payload: hexPayload,
encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
hashFunction: "HASH_FUNCTION_NOT_APPLICABLE" // ED25519 handles hashing internally
}
});
console.log('βœ… Payment intent signed with passkey');
console.log('πŸ” Full Turnkey response:', JSON.stringify(signResult, null, 2));
// Extract signature from Turnkey response - ED25519 might need full signature
const signRawResult = signResult.activity.result.signRawPayloadResult;
const rComponent = signRawResult?.r;
const sComponent = signRawResult?.s;
if (!rComponent) {
throw new Error('No signature returned from Turnkey');
}
// ED25519 signature should be exactly 64 bytes (128 hex chars)
// Turnkey returns r and s as separate 32-byte components
if (!rComponent || !sComponent) {
throw new Error('Missing signature components from Turnkey');
}
// Remove any "0x" prefixes and ensure lowercase
const rClean = rComponent.replace(/^0x/, '').toLowerCase();
const sClean = sComponent.replace(/^0x/, '').toLowerCase();
// Validate component lengths (should be 32 bytes = 64 hex chars each)
if (rClean.length !== 64 || sClean.length !== 64) {
throw new Error(`Invalid signature component lengths: r=${rClean.length}, s=${sClean.length}`);
}
// For ED25519, signature is r||s (concatenated)
const signature = rClean + sClean;
console.log('πŸ” ED25519 signature formatted:', {
rLength: rClean.length,
sLength: sClean.length,
totalLength: signature.length,
signaturePreview: signature.substring(0, 40) + '...',
isValidLength: signature.length === 128
});
return {
signature: signature,
signer: turnkeyWallet.turnkeyAddress,
timestamp: new Date().toISOString(),
type: 'passkey_ed25519'
};
} catch (err) {
console.error('❌ Payment intent signing failed:', err);
throw new Error(`Signing failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
}
};
const testTurnkeyAuthFlow = async (): Promise<{ success: boolean; message: string }> => {
try {
if (!turnkeyWallet) {
return { success: false, message: 'No Turnkey wallet available' };
}
// Test signing a mock payment intent
const mockPaymentIntent = JSON.stringify({
amount: '1000000', // 1 USDC in base units
destination: 'mockAddress123',
timestamp: Date.now()
});
const result = await signWithTurnkey(mockPaymentIntent);
if (result.stamp.publicKey && result.stamp.signature) {
return {
success: true,
message: `Turnkey auth flow test passed. Public key: ${result.stamp.publicKey.slice(0, 10)}...`
};
} else {
return { success: false, message: 'Invalid signature response' };
}
} catch (err) {
return {
success: false,
message: `Turnkey auth test failed: ${err instanceof Error ? err.message : 'Unknown error'}`
};
}
};
const testUSDCFormatting = () => {
try {
const tests = [
{ input: '1.5', expected: '1500000' },
{ input: '0.000001', expected: '1' },
{ input: '100', expected: '100000000' }
];
const results = tests.map(test => {
const baseUnits = usdcToBaseUnits(test.input);
const backToUI = usdcFromBaseUnits(baseUnits);
const formatted = formatUSDC(test.input);
return {
input: test.input,
baseUnits,
backToUI,
formatted,
success: baseUnits === test.expected
};
});
const allPassed = results.every(r => r.success);
return {
success: allPassed,
results
};
} catch (err) {
return {
success: false,
results: { error: err instanceof Error ? err.message : 'Unknown error' }
};
}
};
const testAddressValidation = (address: string): boolean => {
return isValidSolanaAddress(address);
};
// Comprehensive test function for Chunk 1 validation
const runChunk1Tests = async (): Promise<{ success: boolean; results: any[] }> => {
const results = [];
let allPassed = true;
// Test 1: Turnkey authentication
const authTest = await testTurnkeyAuthFlow();
results.push({
test: 'Turnkey Auth Flow',
success: authTest.success,
message: authTest.message
});
if (!authTest.success) allPassed = false;
// Test 2: USDC formatting
const usdcTest = testUSDCFormatting();
results.push({
test: 'USDC Formatting',
success: usdcTest.success,
message: usdcTest.success ? 'All USDC conversions passed' : 'Some USDC conversions failed',
details: usdcTest.results
});
if (!usdcTest.success) allPassed = false;
// Test 3: Address validation
const validAddress = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; // Real USDC mint address
const invalidAddress = 'invalid-address-123';
const validTest = testAddressValidation(validAddress);
const invalidTest = !testAddressValidation(invalidAddress);
results.push({
test: 'Address Validation',
success: validTest && invalidTest,
message: validTest && invalidTest ? 'Address validation working correctly' : 'Address validation failed',
details: { validTest, invalidTest }
});
if (!(validTest && invalidTest)) allPassed = false;
return { success: allPassed, results };
};
const getBalance = async (): Promise<{ usdc: string; sol: string }> => {
if (!smartAccount) {
throw new Error('No smart account found');
}
try {
const balanceData = await gridService.getBalance(smartAccount.smart_account_address) as any;
// Extract USDC and SOL balances from the response
// This format may vary based on Grid's API response structure
const usdcBalance = balanceData.usdc || '0';
const solBalance = balanceData.sol || '0';
return {
usdc: usdcBalance, // Grid already returns decimal format, don't double-convert
sol: solBalance
};
} catch (err) {
console.error('Balance fetch error:', err);
const errorMessage = err instanceof Error ? err.message : 'Failed to get balance';
// For testing purposes, return zero balances instead of throwing
if (errorMessage.includes('not found') || errorMessage.includes('404')) {
return { usdc: '0', sol: '0' };
}
setError(errorMessage);
throw err;
}
};
const getTransferHistory = async (): Promise<any[]> => {
if (!smartAccount) {
throw new Error('No smart account found');
}
try {
const transfers = await gridService.getTransferHistory(smartAccount.smart_account_address) as any[];
return transfers;
} catch (err) {
console.error('Transfer history fetch error:', err);
const errorMessage = err instanceof Error ? err.message : 'Failed to get transfer history';
// For testing purposes, return empty array instead of throwing for new accounts
if (errorMessage.includes('not found') || errorMessage.includes('404')) {
return [];
}
setError(errorMessage);
throw err;
}
};
// Chunk 2: Payment Flow Methods
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;
}
};
const confirmPayment = async (paymentIntent: any, signedPayload: any) => {
if (!smartAccount) {
throw new Error('No smart account found');
}
try {
console.log('πŸ”„ Sending to Grid:', {
intentPayload: paymentIntent.intent_payload ? 'Present' : 'Missing',
signature: signedPayload.signature ? 'Present' : 'Missing'
});
const response = await gridService.confirmPaymentIntent(
smartAccount.smart_account_address,
paymentIntent.id,
{
intentPayload: paymentIntent.intent_payload, // Grid actually expects camelCase despite docs
mpcPayload: JSON.stringify({
signature: signedPayload.signature,
signer: signedPayload.signer,
type: signedPayload.type
}) // Grid expects mpcPayload field even for non-MPC
},
false // Use Grid v0 authorization (not MPC) to match backend
);
return response;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to confirm payment';
setError(errorMessage);
throw err;
}
};
// 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 signedPayload = await signGridPaymentIntent(paymentIntent.intent_payload);
console.log('βœ… Payment signed with Turnkey');
// Step 3: Confirm payment with Grid
const result = await confirmPayment(paymentIntent, signedPayload);
console.log('βœ… Payment confirmed with Grid');
return result;
} catch (err) {
console.error('❌ USDC transfer failed:', err);
throw err;
}
};
// Debug helper to check smart account on-chain
const checkSmartAccountOnChain = async (): Promise<{ exists: boolean; hasSOL: boolean; details: any }> => {
if (!smartAccount) {
throw new Error('No smart account found');
}
try {
// Use a simple RPC call to check if the account exists
const response = await fetch('https://api.mainnet-beta.solana.com', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'getAccountInfo',
params: [
smartAccount.smart_account_address,
{ encoding: 'base64' }
]
})
});
const { result } = await response.json();
if (result?.value) {
const lamports = result.value.lamports;
const hasSOL = lamports > 0;
console.log(`πŸ” Smart Account ${smartAccount.smart_account_address}:`);
console.log(` - Exists: YES`);
console.log(` - SOL Balance: ${lamports / 1e9} SOL`);
console.log(` - Has SOL: ${hasSOL}`);
return {
exists: true,
hasSOL,
details: {
lamports,
solBalance: lamports / 1e9,
executable: result.value.executable,
owner: result.value.owner
}
};
} else {
console.log(`❌ Smart Account ${smartAccount.smart_account_address} does not exist on-chain`);
return { exists: false, hasSOL: false, details: null };
}
} catch (err) {
console.error('Failed to check smart account on-chain:', err);
throw err;
}
};
const clearGridData = async () => {
try {
// Only clear KYC from AsyncStorage - smart account comes from database
await AsyncStorage.removeItem('grid_kyc');
setSmartAccount(null);
setKyc(null);
setError(null);
} catch (err) {
console.error('Error clearing Grid data:', err);
}
};
const refreshSmartAccount = async () => {
try {
console.log('πŸ”„ Refreshing smart account data...');
await loadStoredData();
console.log('βœ… Smart account data refreshed');
} catch (err) {
console.error('Error refreshing smart account data:', err);
}
};
const value: GridContextType = {
smartAccount,
kyc,
isRequestingKyc,
error,
requestKyc,
getKycStatus,
createVirtualAccount,
clearGridData,
refreshSmartAccount,
hasSmartAccount: !!smartAccount,
// Updated authorization and testing methods (now using Turnkey)
testTurnkeyAuthFlow,
testUSDCFormatting,
testAddressValidation,
runChunk1Tests,
// Chunk 2: Payment Flow Methods (now using Turnkey backend)
preparePaymentIntent,
confirmPayment,
getBalance,
getTransferHistory,
sendUSDC,
signWithTurnkey,
// Debug helpers
checkSmartAccountOnChain,
};
return <GridContext.Provider value={value}>{children}</GridContext.Provider>;
}
export function useGrid(): GridContextType {
const context = useContext(GridContext);
if (context === undefined) {
throw new Error('useGrid must be used within a GridProvider');
}
return context;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment